Repository: SwiftKickMobile/SwiftMessages
Branch: master
Commit: c0ff6c65bfc0
Files: 149
Total size: 730.3 KB
Directory structure:
gitextract__w424663/
├── .gitignore
├── CHANGELOG.md
├── Demo/
│ ├── .Podfile
│ ├── Demo/
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── iconSwiftMessages.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── puppies.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── splashBanner.imageset/
│ │ │ └── Contents.json
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── CountedMessageView.swift
│ │ ├── CountedViewController.swift
│ │ ├── ExploreViewController.swift
│ │ ├── Info.plist
│ │ ├── TacoDialogView.swift
│ │ ├── TacoDialogView.xib
│ │ ├── Utils.swift
│ │ ├── ViewController.swift
│ │ └── ViewControllersViewController.swift
│ └── Demo.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ └── Demo.xcscheme
├── Design/
│ ├── SwiftMessages Demo App Icon.sketch
│ └── SwiftMessagesDesign.sketch
├── LICENSE.md
├── Package.swift
├── README.md
├── SwiftMessages/
│ ├── AccessibleMessage.swift
│ ├── Animator.swift
│ ├── BackgroundViewable.swift
│ ├── BaseView.swift
│ ├── CALayer+Extensions.swift
│ ├── CornerRoundingView.swift
│ ├── Error.swift
│ ├── HapticMessage.swift
│ ├── Identifiable.swift
│ ├── Info.plist
│ ├── KeyboardTrackingView.swift
│ ├── MarginAdjustable+Extensions.swift
│ ├── MarginAdjustable.swift
│ ├── MaskingView.swift
│ ├── MessageGeometryProxy.swift
│ ├── MessageHostingView.swift
│ ├── MessageView.swift
│ ├── MessageViewConvertible.swift
│ ├── NSBundle+Extensions.swift
│ ├── NSLayoutConstraint+Extensions.swift
│ ├── PassthroughView.swift
│ ├── PassthroughWindow.swift
│ ├── PhysicsAnimation.swift
│ ├── PhysicsPanHandler.swift
│ ├── Presenter.swift
│ ├── Resources/
│ │ ├── CardView.xib
│ │ ├── CenteredView.xib
│ │ ├── MessageView.xib
│ │ ├── StatusLine.xib
│ │ └── TabView.xib
│ ├── SwiftMessageModifier.swift
│ ├── SwiftMessages.Config+Extensions.swift
│ ├── SwiftMessages.h
│ ├── SwiftMessages.swift
│ ├── SwiftMessagesHideAction.swift
│ ├── SwiftMessagesSegue.swift
│ ├── Task+Extensions.swift
│ ├── Theme.swift
│ ├── TopBottomAnimation.swift
│ ├── TopBottomAnimationStyle.swift
│ ├── TopBottomPresentable.swift
│ ├── UIEdgeInsets+Extensions.swift
│ ├── UIViewController+Extensions.swift
│ ├── UIWindow+Extensions.swift
│ ├── Weak.swift
│ ├── WindowScene.swift
│ └── WindowViewController.swift
├── SwiftMessages.podspec
├── SwiftMessages.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ └── SwiftMessages.xcscheme
├── SwiftMessagesTests/
│ ├── Info.plist
│ └── SwiftMessagesTests.swift
├── SwiftUIDemo/
│ ├── SwiftUIDemo/
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── Demo message background.colorset/
│ │ │ └── Contents.json
│ │ ├── DemoMessage.swift
│ │ ├── DemoMessageView.swift
│ │ ├── DemoMessageWithButtonView.swift
│ │ ├── DemoView.swift
│ │ ├── Info.plist
│ │ ├── Preview Content/
│ │ │ └── Preview Assets.xcassets/
│ │ │ └── Contents.json
│ │ └── SwiftUIDemoApp.swift
│ └── SwiftUIDemo.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── ViewControllers.md
└── iMessageDemo/
├── .Podfile
├── Podfile
├── Pods/
│ ├── Local Podspecs/
│ │ └── SwiftMessages.podspec.json
│ ├── Pods.xcodeproj/
│ │ ├── project.pbxproj
│ │ └── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── Target Support Files/
│ ├── Pods-iMessageDemo/
│ │ ├── Pods-iMessageDemo-Info.plist
│ │ ├── Pods-iMessageDemo-acknowledgements.markdown
│ │ ├── Pods-iMessageDemo-acknowledgements.plist
│ │ ├── Pods-iMessageDemo-dummy.m
│ │ ├── Pods-iMessageDemo-frameworks-Debug-input-files.xcfilelist
│ │ ├── Pods-iMessageDemo-frameworks-Debug-output-files.xcfilelist
│ │ ├── Pods-iMessageDemo-frameworks-Release-input-files.xcfilelist
│ │ ├── Pods-iMessageDemo-frameworks-Release-output-files.xcfilelist
│ │ ├── Pods-iMessageDemo-frameworks.sh
│ │ ├── Pods-iMessageDemo-umbrella.h
│ │ ├── Pods-iMessageDemo.debug.xcconfig
│ │ ├── Pods-iMessageDemo.modulemap
│ │ └── Pods-iMessageDemo.release.xcconfig
│ ├── Pods-iMessageExtensionDemo/
│ │ ├── Pods-iMessageExtensionDemo-Info.plist
│ │ ├── Pods-iMessageExtensionDemo-acknowledgements.markdown
│ │ ├── Pods-iMessageExtensionDemo-acknowledgements.plist
│ │ ├── Pods-iMessageExtensionDemo-dummy.m
│ │ ├── Pods-iMessageExtensionDemo-umbrella.h
│ │ ├── Pods-iMessageExtensionDemo.debug.xcconfig
│ │ ├── Pods-iMessageExtensionDemo.modulemap
│ │ └── Pods-iMessageExtensionDemo.release.xcconfig
│ └── SwiftMessages/
│ ├── ResourceBundle-SwiftMessages-SwiftMessages-Info.plist
│ ├── ResourceBundle-SwiftMessages_SwiftMessages-SwiftMessages-Info.plist
│ ├── SwiftMessages-Info.plist
│ ├── SwiftMessages-dummy.m
│ ├── SwiftMessages-prefix.pch
│ ├── SwiftMessages-umbrella.h
│ ├── SwiftMessages.debug.xcconfig
│ ├── SwiftMessages.modulemap
│ └── SwiftMessages.release.xcconfig
├── iMessageDemo/
│ ├── AppDelegate.swift
│ ├── Info.plist
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── iMessageDemo.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ └── iMessageExtensionDemo.xcscheme
├── iMessageDemo.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── IDEWorkspaceChecks.plist
│ └── WorkspaceSettings.xcsettings
└── iMessageExtensionDemo/
├── Base.lproj/
│ └── MainInterface.storyboard
├── Info.plist
└── MessagesViewController.swift
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.swiftpm/
.DS_Store
*.mobileprovision
*.cer
*.certSigningRequest
*.p12
*.pem
*.pkey
screenshots
rvm.env
.build
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
## 10.0.2
### Features
* #549 Create a formal dismissal mechanism for SwiftUI message. Use `@Environment(\.swiftMessagesHide) private var hide` then call `hide(animated: true)` to dismiss.
* #571 PhysicsPanHandler.swift make configure public.
### Fixes
* #581 [BUG]: SwiftUI message views don't respond to touch input on Xcode 26 beta 6 iOS 26 simulator.
* #581 [BUG]: SwiftUI message views don't respond to touch input on Xcode 26 beta 6 iOS 26 simulator
* #577 Hide Background Elements from Modal SwiftMessage in VoiceOver mode
* #578 Fix crash on iPadOS 18+ when presenting bottom SwiftMessage above UITabBarController
### Features
* #523 Add a `priority` configuration option.
* #548 Adds 'TopBottomPresentable' protocol to allow animators implementation to reuse 'top/bottom' integration in presentation
* #543 Make the `SwiftMessages` initializer `nonisolated` to improve interoperability with dependency injection frameworks like Factory.
* #560 Add a new `swiftMessage` modifier variation that provides a ` MessageGeometryProxy` type to the message view builder—this works around an inssue with `GeometryReader` not working in `UIHostingController`.
### Fixes
* Fix broken touch handling in iOS 18.
## 10.0.0
### Features
* Add a variation on the `.swiftMessage()` modifier that takes a view builder instead of requiring that the bound value conform to `MessageViewConvertible`. This syntax is more similar to the familiar `sheet()` modifier syntax and provides more flexibility for constructing message views.
* #207 Add optional haptic feedback
### Changes
* Use `@MainActor` to ensure that SwiftMessages is not called from a background queue.
* Bump minimum deployment target to iOS 13.
### Fixes
* #535 window being accessed from background thread when dequeueNext is called
* #534 Xcode warnings in two swift files
* #533 How do I show a message that appears above the keyboard, when the keyboard is already visible?
## 9.0.9
### Fixes
* Fix hit testing on SwiftUI views to allow touches around the view's margins to pass through to the underlying view.
* Update `KeyboardTrackingView` to continue tracking the keyboard even when not installed in the view hierarchy.
## 9.0.8
### Changes
* #529 Update readme and SwiftUI demo to demostrate how to mask edges.
## 9.0.7
### Features
* Added support for SwiftUI
### Fixes
* #527 Crash while clicking two times to hide the presenting controller
* #517 Prevent orphaned views from blocking the queue
* Prevent orphaned `SwiftMessagesSeque`s from retaining the presenting view controller
## 9.0.6
### Features
* Add `UIView` associated type to `Event`, e.g. `willShow(UIView)` so that event listeners can inspect the view.
* Add `Event.id: String?` property so that event listeners can reason about the view's ID.
## 9.0.5
### Fixes
* #482 Fix timing of `KeyboardTrackingView` callbacks.
* #483 KeyboardTrackingView causes a small space under bottom-style view
## 9.0.4
* #471 Xcode 13 issue - Enum cases with associated values cannot be marked potentially unavailable with '@available'
* Improve colors for dark mode.
## 9.0.3
### Fixes
* #467 Lower or equal level window's views disappear upon hide
* #466 Alert not shown after Biometry check
* #465 Fix broken Carthage build. The Carthage build was broken due to the `iMessageDemo` project's use of CocoaPods and the automatically generated `SwiftMessages` framework scheme created by CocoaPods. The podfile was modified to delete this scheme, but Carthage users may need to run `pod install` on the `iMessagesDemo` project, if they have CocoaPods installed, or manually delete the `iMessageDemo/Pods/Pods.xcodeproj/xcuserdata` folder.
## 9.0.2
### Fixes
* Fix app extension compile error when using CocoaPods.
## 9.0.1
### Fixes
* #455 #458 Restore key window after message is interacted with. When a message becomes the key window, such as if the user interacts with the message, iOS does not automatically restore the previous key window when the message is dismissed. SwiftMessages has some logic in `WindowViewController` to restore the key window. This change makes that logic more robust.
## 9.0.0
### Features
* #447 Add the ability to show view controller in a new window with `SwiftMessagesSegue`.
This capability is available when using `SwiftMessagesSegue` programmatically by supplying
an instance of `WindowViewController` as the segue's source view controller.
### Changes
* This release has minor breaking changes in the `WindowViewController` initializers.
The `windowLevel` is no longer accepted as an argument because the `config` parameter
should specify the window level in the `presentationContext` property.
### Fixes
* #451 Fix app extension crash
## 8.0.5
### Fixes
* #446 Restore previous key window on dismissal if the message assumed key window status.
## 8.0.4
### Features
* #442 Add `MarginAdjustable.respectSafeArea` option to exclude safe area from layout margins.
* #430 Support disable `becomeKeyWindow` from SwiftMessages.Config. This is a workaround for potential issues with apps that display additional windows.
### Fixes
* #437 Revert to explicitly specifying "SwiftMessages" as the module in nib files.
* #440 Fix crash when using SwiftMessages in app extension
## 8.0.3
### Features
* Full support for Swift Package Manager
### Fixes
* #328 ignoreDuplicates is not working
* #412 Fix deployment target on nib files to match target
## 8.0.2
### Changes
* [#395](https://github.com/SwiftKickMobile/SwiftMessages/pull/395) Add preliminary support for Swift Package Manager.
## 8.0.1
### Fixes
* #401 UIAlertController pops up but SwiftMessage layer absorbs all touches.
## 8.0.0
### Changes
* Add `SwiftMessages.PresentationContext.windowScene` option for targeting a specific window scene.
* Changed the behavior of the default `presentationContext`, `.automatic`. Previously, if the root view controller was presenting, the message would only be displayed over the presented view controller if the `modalPresentationStyle` was `fullScreen` or `overFullScreen`. Now, messages are always displayed over presented view controllers.
* Made `showDuraton` and `hideDuration` on `Animator` non-optional.
* Made `showDuraton` and `hideDuration` writable options on `TopBottomAnimation` and `PhysicsAnimation`.
### Fixes
* #365 Fix an issue with customized `TopBottomAnimation` where messages weren't properly displayed under navigation and tab bars.
* #352 Fix accessibility for view controllers presented with `SwiftMessagesSegue`.
* #355 Update card view layout to support centering of pure textual content
* #354 Support `overrideUserInterfaceStyle` when view presented in its own window
* #360 Fix touch handing issue in iOS 13.1.3
* #382 Fix warnings in Xcode 11.4
## 7.0.1
### Changes
* Support iOS 13.
### Features
* #335 Add option to hide status bar when view is displayed in a window. As of iOS 13, windows can no longer cover the status bar. The only alternative is to set `Config.prefersStatusBarHidden = true` to hide it.
## 7.0.0
### Changes
* Swift 5
* Remove deprecated APIs
### Features
* #313 Improved sizing on iPad
>`SwiftMessagesSegue` provides default view controller sizing based on device, with width on iPad being limited to 500pt max. However, it is recommended that you explicitly specify size appropriate for your content using one of the following methods.
> 1. Define sufficient width and height constraints in your view controller such that it sizes itself.
> 1. Set the `preferredContentSize` property (a.k.a "Use Preferred Explicit Size" in Interface Builder's attribute inspector). Zeros are ignored, e.g. `CGSize(width: 0, height: 350)` only affects the height.
> 1. Add explicit width and/or height constraints to `segue.messageView.backgroundView`.
>
>Note that `Layout.topMessage` and `Layout.bottomMessage` are always full screen width. For other layouts, the there is a maximum 500pt width on for regular horizontal size class (iPad) at 950 priority. This limit can be overridden by adding higher-priority constraints.
* #275 Add ability to avoid the keyboard.
>The `KeyboardTrackingView` class can be used to cause the message view to avoid the keyboard by sliding up when the keyboard gets too close.
>
>````swift
>// Message view
>var config = SwiftMessages.defaultConfig
>config.keyboardTrackingView = KeyboardTrackingView()
>
>// Or view controller
>segue.keyboardTrackingView = KeyboardTrackingView()
>````
>You can incorporate `KeyboardTrackingView` into your app even when you're not using SwiftMessages. Install into your view hierarchy by pinning `KeyboardTrackingView` to the bottom, leading, and trailing edges of the screen. Then pin the bottom of your content that should avoid the keyboard to the top `KeyboardTrackingView`. Use an equality constraint to strictly track the keyboard or an inequality constraint to only move when the keyboard gets too close. `KeyboardTrackingView` works by observing keyboard notifications and adjusting its height to maintain its top edge above the keyboard, thereby pushing your content up. See the comments in `KeyboardTrackingView` for configuration options.
* #276 Add ability to hide message without animation
* #272 Add duration for `SwiftMessagesSegue`
* #278 Make pan gesture recognizers public
## 6.0.2
### Features
* #262 Add event listeners to `SwiftMessagesSegue`.
## 6.0.1
### Features
* #257 The `.centered` presentation style, which is a shortcut for a specific configuration of the `PhysicsAnimation` animator, provides a physics-based dismissal gesture where the view can be flung off screen. When the view goes out of the container view's bounds, the animator calls `SwiftMessages.hide()`, which animates the dim view away and concludes the message view's lifecycle. There is currently a small delay of 0.2s before calling `hide()`.
This change adds the ability to configure the delay by customizing the animator. For example, to set the delay to zero, one would do:
````swift
let animation = PhysicsAnimation()
animation.panHandler.hideDelay = 0
config.presentationStyle = .custom(animator: animation)
````
## 6.0.0
### Changes
* Migrate to Swift 4.2
### Fixes
* Fix #228 restore shared SwiftMessages scheme
## 5.0.1
### Fixes
* Remove debug code that broke the view controller's section of the Demo app.
## 5.0.0
### Breaking Changes
* Removed support for iOS 8.
### Features
* Add support for modal view controller presentation using [`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) custom segue subclass. Try it out in the "View Controllers" section of the Demo app. In addition to the class documentation, more can be found in the [View Controllers](./ViewControllers.md) readme.
* Update nib files to be more visually consistent with iPhone X:
* Introduce [`CornerRoundingView`](./SwiftMessages/CornerRoundingView.swift), which provides configurable corner rounding using squircles (the smoother method of rounding corners that you see on app icons). Nib files that feature rounded corners have their `backgroundView` assigned to a `CornerRoundingView`. `CornerRoundingView` provides a `roundsLeadingCorners` option to dynamically round only the leading corners of the view when presented from top or bottom (a feature used for the tab-style layouts).
* Increased the default corner radius to 20. Corner radius can be changed by either modifying the nib file or
* Reworked the [`MarginAdjustable`](./SwiftMessages/MarginAdjustable.swift) to improve configurability of layout margins.
* Add rubber-banding to the interactive dismissal gesture. Rubber banding is automatically applied for views where `backgroundView` is inset from the message view's edges.
* Added `showDuration` and `hideDuration` properties to the `Animator` protocol (with default implementation that returns `nil`). These values enable animations to work for view controller presentation.
### Fixes
* #202 bodyLabel should set textAlignment to .natural
* #200 Automatic Presentation Context Broken
* Fix default value of `TopBottomAnimation.closePercentThreshold`
## 4.1.4
### Bug Fixes
* Fix #191 Prevent usage of UIApplication.shared when building for extensions
### Improvements
* #192 Add a way to test compilation with app extension
## 4.1.3
### Features
* #183 Added iOS app extension support at compile time.
### Bug Fixes
* Fix #185 Incorrect margin adjustments in landscape
* Fix #188 Physics animation visual glitch
## 4.1.2
### Features
* Updates for Swift 4.1
* #164 Added an optional `windowViewController` property to `SwiftMessages.Config` for supplying a custom subclass of `WindowViewController`.
### Bug Fixes
* Custom presentation styles using `TopBottomAnimation` now display properly under top and bottom bars.
## 4.1.1
### Features
* #152 Get current message being displayed without specifying an `id`
## 4.1.0
### Features
* Fix #134 add support for `CenterAnimation` displayed on top or bottom instead of center (renamed to `PhysicsAnimation`).
### Fixes
* Fix #128 move icons out of asset catalog to prevent mysterious crash
* Fix #129 adjust layout margins on orientation change to preserve layout when iOS hides status bar in landscape.
* Fix #131 by always completing hide/show animations if application isn't active.
## 4.0.0
### Features
* Swift 4.0 syntax
* Added support for iOS 11 and iPhone X. From the readme:
SwiftMessages 4 supports iOS 11 out-of-the-box with built-in support for safe areas. To ensur that message view layouts look just right when overlapping safe areas, views that adopt the `MarginAdjustable` protocol (like `MessageView`) will have their layout margins automatically adjusted by SwiftMessages. However, there is no one-size-fits-all adjustment, so the following properties were added to `MarginAdjustable` to allow for additional adjustments to be made to the layout margins:
````swift
public protocol MarginAdjustable {
...
/// Safe area top adjustment in iOS 11+
var safeAreaTopOffset: CGFloat { get set }
/// Safe area bottom adjustment in iOS 11+
var safeAreaBottomOffset: CGFloat { get set }
}
````
If you're using using custom nib files or view classes and your layouts don't look quite right, try adjusting the values of these properties. `BaseView` (the super class of `MessageView`) declares these properties to be `@IBDesignable` and you can find sample values in the nib files included with SwiftMessages.
### Bug Fixes
* Fix #100 memory leak.
* Change `Layout` enum capitalization to current Swift conventions.
## [3.5.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.5.0)
### Bug Fixes
* Undo change that broke `MessageView` class reference on nib files copied out of the SwiftMessages framework.
## [3.5.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.5.0)
### Features
* Added `SwiftMessages.hideCounted(id:)` method of hiding. The counted method hides when the number of calls to `show()` and `hideCounted(id:)` for a
given message ID are equal. This can be useful for messages that may be
shown from multiple code paths to ensure that all paths are ready to hide.
Also added `SwiftMessages.count(id:)` to get the current count and `SwiftMessages.set(id:count:)` to set the current count.
* Added ways to retrieve message views currently being shown, hidden, or queued to be shown.
````swift
// Get a message view with the given ID if it is currently
// being shown or hidden.
if let view = SwiftMessages.current(id: "some id") { ... }
// Get a message view with the given ID if is it currently
// queued to be shown.
if let view = SwiftMessages.queued(id: "some id") { ... }
// Get a message view with the given ID if it is currently being
// shown, hidden or in the queue to be shown.
if let view = SwiftMessages.currentOrQueued(id: "some id") { ... }
````
### Bug Fixes
* Fix #116 for message views that don't adopt the `Identifiable` protocol by using the memory address as the ID.
* Fix #113 MessageView not hiding
* Fix #87 Support manual install
## [3.4.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.4.0)
### Features
* Added `.center` presentation style with a physics-based dismissal gesture.
* 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.
* Added `.centered` message view layout with elements centered and arranged vertically.
* Added `configureBackgroundView(width:)` and `configureBackgroundView(sideMargin:)` convenience methods to `MessageView`.
## [3.3.4](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.4)
### Features
* #89 Add `blur` dim mode option.
### Bug Fixes
* #98 Fix touch handling in message view's background view.
* #97 Fix main thread checker warning
## [3.3.3](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.3)
### Bug Fixes
* Fix an issue where rapidly showing and hiding messages could result in messages becoming orphaned on-screen.
## [3.3.2](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.2)
### Improvements
* `MessageView` is smarter about including additional accessibility views for cases where you've added accessible elements to the view. Previously only the `button` was included. Now all views where `isAccessibilityElement == true`.
Note that all nib files now have `isAccessibilityElement == false` for `titleLabel`, `bodyLabel` and `iconLabel` (`titleLabel` and `bodyLabel` are read out as part of the overall message view's text). If any of these need to be directly accessible, then copy the nib file into your project and select "Enabled" in the Accessibility section of the Identity Inspector.
## [3.3.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.1)
### Bug Fixes
* Fix regression where the UI was being blocked when using `DimMode.none`.
## [3.3.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.0)
### Features
* Add proper support for VoiceOver. See the [Accessibility section](README.md#accessibility) of the readme.
## [3.2.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.2.1)
### Bug Fixes
* Fix infinite loop bug introduced in 3.2.0.
## [3.2.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.2.0)
### Features
* Added the ability to display messages for an indefinite duration while enforcing a minimum duration using `Duration.indefinite(delay:minimum)`.
This option is useful for displaying a message when a process is taking too long but you don't want to display the message if the process completes in a reasonable amount of time.
For example, if a URL load is expected to complete in 2 seconds, you may use the value `unknown(delay: 2, minimum 1)` to ensure that the message will not be displayed most of the time, but will be displayed for at least 1 second if the operation takes longer than 2 seconds. By specifying a minimum duration, you can avoid hiding the message too fast if the operation finishes right after the delay interval.
### Bug Fixes
* Prevent views below the dim view from receiving accessibility focus.
* Prevent taps in the message view from hiding when using interactive dim mode.
* Fix memory leak of single message view
## [3.1.5](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.5)
### Bug Fixes
* Fix memory leak of `MessageViews`.
## [3.1.4](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.4)
### Bug Fixes
* Fixed an issue where UIKit components were being instantiated off the main queue.
## [3.1.3](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.3)
### Features
* Added the ability to set a custom value on `MessageView.id`. This can be useful for dismissing specific messages using a pre-defined `id`.
## [3.1.2](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.2)
### Features
* Changed the default behavior when a message is displayed in its own window (such as with `.window` presentation context) to no longer become the key window in order to prevent the keyboard from dismissing. If one needs the message's window to become key, this can be done by setting `SwiftMessages.Config.becomeKeyWindow` to `true`. See
### Bug Fixes
* Changed the internal logic of hiding a message view to always succeed to work around the problem of the hide animation failing, such as when started while the app is not active.
* Improved reliability of the automatic adjustments made to avoid message views overlapping the status bar, particularly when using the `.view` presentation context.
## [3.1.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.1)
### Features
* Added a `view` case to `presentationContext` for displaying the message in a specific container view.
## [3.1.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.1.0)
### Features
* Add `eventListeners` option to `SwiftMessages.Config` to specify a list of event listeners to be notified of message show and hide events.
## [3.0.3](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.0.3)
### Features
* Add `ignoreDuplicates` option to `SwiftMessages.Config` to specify whether or not to ignore duplicate `Identifiable` message views.
## [3.0.2](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.0.2)
### Features
* Add `shouldAutorotate` option to `SwiftMessages.Config` for customizing device rotation behavior when messages are presented in an overlay window. By default, message will auto-rotate.
## [3.0.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.0.1)
### Improvements
* Enable automatic provisioning on framework target
## [3.0.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.0.0)
### Breaking Changes
* Support for Swift 3.0.
## [2.0.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/2.0.0)
### Breaking Changes
* Support for Swift 2.3 and Xcode 8.
The nib files needed to be updated to Xcode 8 format to work
around an iOS bug. Unfortunately, this makes it necessary
to break backward compatibility with Swift 2.2 and Xcode 7.
## [1.1.4](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.1.4)
### Bug Fixes
* Fix #16 Preserve status bar visibility when displaying message in a new window.
## [1.1.3](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.1.3)
### Features
* Add default configuration `SwiftMessages.defaultConfig` that can be used when calling the variants of `show()` that don't take a `config` argument or as a global base for custom configs.
* Add `Array.sm_random()` function that returns a random element from the array. Can be used to create a playful
message that cycles randomly through a set of emoji icons, for example.
### Bug Fixes
* Fix #5 Emoji not shown!
* Fix #6 There is no way to create SwiftMessages instance as there is no public initializer
## [1.1.2](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.1.2)
### Bug Fixes
* Fix Carthage-related issues.
## [1.1.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.1.1)
### Features
* New layout `Layout.TabView` — like `Layout.CardView` with one end attached to the super view.
### Bug Fixes
* Fix spacing between title and body text in `Layout.CardView` when body text wraps.
## [1.1.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.1.0)
### Improvements
### API Changes
* The `BaseView.contentView` property of was removed because it no longer had any functionality in the framework.
This is a minor backwards incompatible change. If you've copied one of the included nib files from a previous release, you may get a key-value coding runtime error related to contentView, in which case you can subclass the view and add a `contentView` property or you can remove the outlet connection in Interface Builder.
## [1.0.3](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.0.2)
### Improvements
* Remove the `iconContainer` property from `MessageView`.
### Bug Fixes
* Fix constraints generated by `BaseView.installContentView()`.
## [1.0.2](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.0.2)
### Features
* Add support for specifying an `IconStyle` in the `MessageView.configureTheme()` convenience function.
## [1.0.1](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.0.1)
* Add code comments.
## [1.0.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/1.0.0)
* Initial release.
================================================
FILE: Demo/.Podfile
================================================
target 'Demo' do
use_frameworks!
workspace 'Demo.xcworkspace'
xcodeproj 'Demo.xcodeproj'
pod 'SwiftMessages/App', :path => '../'
pod 'SwiftMessages/SegueExtras', :path => '../'
end
================================================
FILE: Demo/Demo/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Demo
//
// Created by Tim Moose on 8/11/16.
// Copyright © 2016 SwiftKick Mobile. All rights reserved.
//
import UIKit
let brandColor = UIColor(red: 42/255.0, green: 168/255.0, blue: 250/255.0, alpha: 1)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
window?.tintColor = brandColor
UISwitch.appearance().onTintColor = brandColor
return true
}
}
================================================
FILE: Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x-1.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x-2.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-iTunes.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Demo/Demo/Assets.xcassets/Contents.json
================================================
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"filename" : "iconSwiftMessages.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Demo/Demo/Assets.xcassets/puppies.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "puppies.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: Demo/Demo/Assets.xcassets/splashBanner.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"filename" : "splashBanner.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "original"
}
}
================================================
FILE: Demo/Demo/Base.lproj/LaunchScreen.storyboard
================================================
================================================
FILE: Demo/Demo/Base.lproj/Main.storyboard
================================================
================================================
FILE: Demo/Demo/CountedMessageView.swift
================================================
//
// CountedMessageView.swift
// Demo
//
// Created by Timothy Moose on 8/25/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class CountedMessageView: UIView, Identifiable {
@IBOutlet weak var countLabel: UILabel!
var id: String {
return "counted"
}
}
================================================
FILE: Demo/Demo/CountedViewController.swift
================================================
//
// CountedViewController.swift
// Demo
//
// Created by Timothy Moose on 8/25/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class CountedViewController: UIViewController {
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet var messageView: CountedMessageView!
@IBOutlet weak var messageContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
descriptionLabel.configureBodyTextStyle()
descriptionLabel.configureCodeStyle(on: "show()")
descriptionLabel.configureCodeStyle(on: "hideCounted(id:)")
}
@IBAction func show() {
var config = SwiftMessages.defaultConfig
config.presentationStyle = .center
config.duration = .forever
config.presentationContext = .view(messageContainer)
SwiftMessages.show(config: config, view: messageView)
updateCountLabel()
}
@IBAction func hide() {
SwiftMessages.hideCounted(id: messageView.id)
updateCountLabel()
}
private func updateCountLabel() {
let count = SwiftMessages.count(id: messageView.id)
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .spellOut
messageView.countLabel.text = numberFormatter.string(from: NSNumber(value: count))?.uppercased()
}
}
================================================
FILE: Demo/Demo/ExploreViewController.swift
================================================
//
// ExploreViewController.swift
// Demo
//
// Created by Tim Moose on 8/13/16.
// Copyright © 2016 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class ExploreViewController: UITableViewController, UITextFieldDelegate {
@IBAction func show(_ sender: AnyObject) {
// View setup
let view: MessageView
switch layout.selectedSegmentIndex {
case 1:
view = MessageView.viewFromNib(layout: .cardView)
case 2:
view = MessageView.viewFromNib(layout: .tabView)
case 3:
view = MessageView.viewFromNib(layout: .statusLine)
default:
view = try! SwiftMessages.viewFromNib()
}
view.configureContent(title: titleText.text, body: bodyText.text, iconImage: nil, iconText: nil, buttonImage: nil, buttonTitle: "Hide", buttonTapHandler: { _ in SwiftMessages.hide() })
let iconStyle: IconStyle
switch self.iconStyle.selectedSegmentIndex {
case 1:
iconStyle = .light
case 2:
iconStyle = .subtle
default:
iconStyle = .default
}
switch theme.selectedSegmentIndex {
case 0:
view.configureTheme(.info, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn)
view.accessibilityPrefix = "info"
case 1:
view.configureTheme(.success, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn)
view.accessibilityPrefix = "success"
case 2:
view.configureTheme(.warning, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn)
view.accessibilityPrefix = "warning"
case 3:
view.configureTheme(.error, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn)
view.accessibilityPrefix = "error"
default:
let iconText = ["🐸", "🐷", "🐬", "🐠", "🐍", "🐹", "🐼"].randomElement()
view.configureTheme(backgroundColor: UIColor.purple, foregroundColor: UIColor.white, iconImage: nil, iconText: iconText)
view.button?.setImage(Icon.errorSubtle.image, for: .normal)
view.button?.setTitle(nil, for: .normal)
view.button?.backgroundColor = UIColor.clear
view.button?.tintColor = UIColor.green.withAlphaComponent(0.7)
}
if dropShadow.isOn {
view.configureDropShadow()
}
if !showButton.isOn {
view.button?.isHidden = true
}
if !showIcon.isOn {
view.iconImageView?.isHidden = true
view.iconLabel?.isHidden = true
}
if !showTitle.isOn {
view.titleLabel?.isHidden = true
}
if !showBody.isOn {
view.bodyLabel?.isHidden = true
}
// Config setup
var config = SwiftMessages.defaultConfig
switch presentationStyle.selectedSegmentIndex {
case 1:
config.presentationStyle = .bottom
case 2:
config.presentationStyle = .center
default:
break
}
switch presentationContext.selectedSegmentIndex {
case 1:
config.presentationContext = .window(windowLevel: UIWindow.Level.normal)
case 2:
config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
default:
break
}
switch duration.selectedSegmentIndex {
case 1:
config.duration = .forever
case 2:
config.duration = .seconds(seconds: 1)
case 3:
config.duration = .seconds(seconds: 5)
default:
break
}
switch dimMode.selectedSegmentIndex {
case 1:
config.dimMode = .gray(interactive: true)
case 2:
config.dimMode = .color(color: #colorLiteral(red: 0.1019607857, green: 0.2784313858, blue: 0.400000006, alpha: 0.7477525685), interactive: true)
case 3:
config.dimMode = .blur(style: .dark, alpha: 1.0, interactive: true)
default:
break
}
config.shouldAutorotate = self.autoRotate.isOn
config.interactiveHide = interactiveHide.isOn
// Set status bar style unless using card view (since it doesn't
// go behind the status bar).
if case .top = config.presentationStyle, layout.selectedSegmentIndex != 1 {
switch theme.selectedSegmentIndex {
case 1...4:
config.preferredStatusBarStyle = .lightContent
default:
break
}
}
if view.defaultHaptic == nil && hapticFeedback.isOn {
config.haptic = .success
}
// Show
SwiftMessages.show(config: config, view: view)
}
@IBAction func hide(_ sender: AnyObject) {
SwiftMessages.hide()
}
@IBOutlet weak var presentationStyle: UISegmentedControl!
@IBOutlet weak var presentationContext: UISegmentedControl!
@IBOutlet weak var duration: UISegmentedControl!
@IBOutlet weak var dimMode: UISegmentedControl!
@IBOutlet weak var interactiveHide: UISwitch!
@IBOutlet weak var hapticFeedback: UISwitch!
@IBOutlet weak var layout: UISegmentedControl!
@IBOutlet weak var theme: UISegmentedControl!
@IBOutlet weak var iconStyle: UISegmentedControl!
@IBOutlet weak var autoRotate: UISwitch!
@IBOutlet weak var dropShadow: UISwitch!
@IBOutlet weak var titleText: UITextField!
@IBOutlet weak var bodyText: UITextField!
@IBOutlet weak var showButton: UISwitch!
@IBOutlet weak var showIcon: UISwitch!
@IBOutlet weak var showTitle: UISwitch!
@IBOutlet weak var showBody: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
titleText.delegate = self
bodyText.delegate = self
}
/*
MARK: - UITextFieldDelegate
*/
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
return true
}
}
================================================
FILE: Demo/Demo/Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
SM Demo
CFBundlePackageType
APPL
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1
LSRequiresIPhoneOS
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UIRequiredDeviceCapabilities
armv7
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UIInterfaceOrientationPortraitUpsideDown
================================================
FILE: Demo/Demo/TacoDialogView.swift
================================================
//
// TacoDialogView.swift
// Demo
//
// Created by Tim Moose on 8/12/16.
// Copyright © 2016 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class TacoDialogView: MessageView {
fileprivate static var tacoTitles = [
1 : "Just one, Please",
2 : "Make it two!",
3 : "Three!!!",
4 : "Cuatro!!!!",
]
var getTacosAction: ((_ count: Int) -> Void)?
var cancelAction: (() -> Void)?
fileprivate var count = 1 {
didSet {
iconLabel?.text = String(repeating: "🌮", count: count)//String(count: count, repeatedValue: )
bodyLabel?.text = TacoDialogView.tacoTitles[count] ?? "\(count)" + String(repeating: "!", count: count)
}
}
@IBAction func getTacos() {
getTacosAction?(Int(tacoSlider.value))
}
@IBAction func cancel() {
cancelAction?()
}
@IBOutlet weak var tacoSlider: UISlider!
@IBAction func tacoSliderSlid(_ slider: UISlider) {
count = Int(slider.value)
}
@IBAction func tacoSliderFinished(_ slider: UISlider) {
slider.setValue(Float(count), animated: true)
}
}
================================================
FILE: Demo/Demo/TacoDialogView.xib
================================================
================================================
FILE: Demo/Demo/Utils.swift
================================================
//
// Utils.swift
// Demo
//
// Created by Timothy Moose on 8/25/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension UILabel {
func configureBodyTextStyle() {
let bodyStyle = NSMutableParagraphStyle()
bodyStyle.lineSpacing = 5.0
attributedText = NSAttributedString(string: text ?? "", attributes: [NSAttributedString.Key.paragraphStyle : bodyStyle])
}
func configureCodeStyle(on substring: String?) {
var attributes: [NSAttributedString.Key : Any] = [:]
let codeFont = UIFont(name: "CourierNewPSMT", size: font.pointSize)!
attributes[NSAttributedString.Key.font] = codeFont
attributes[NSAttributedString.Key.backgroundColor] = UIColor(white: 0.96, alpha: 1)
attributedText = attributedText?.setAttributes(attributes: attributes, onSubstring: substring)
}
}
extension NSAttributedString {
public func setAttributes(attributes: [NSAttributedString.Key : Any], onSubstring substring: String?) -> NSAttributedString {
let mutableSelf = NSMutableAttributedString(attributedString: self)
if let substring = substring {
var range = NSRange()
repeat {
let length = mutableSelf.length
let start = range.location + range.length
let remainingLength = length - start
let remainingRange = NSRange(location: start, length: remainingLength)
range = (mutableSelf.string as NSString).range(of: substring, options: .caseInsensitive, range: remainingRange)
NSAttributedString.set(attributes: attributes, in: range, of: mutableSelf)
} while range.length > 0
} else {
let range = NSRange(location: 0, length: mutableSelf.length)
NSAttributedString.set(attributes: attributes, in: range, of: mutableSelf)
}
return mutableSelf
}
private static func set(attributes newAttributes: [NSAttributedString.Key : Any], in range: NSRange, of mutableString: NSMutableAttributedString) {
if range.length > 0 {
var attributes = mutableString.attributes(at: range.location, effectiveRange: nil)
for (key, value) in newAttributes {
attributes.updateValue(value, forKey: key)
}
mutableString.setAttributes(attributes, range: range)
}
}
}
================================================
FILE: Demo/Demo/ViewController.swift
================================================
//
// ViewController.swift
// Demo
//
// Created by Tim Moose on 8/11/16.
// Copyright © 2016 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class ViewController: UITableViewController {
var items: [Item] = [
.titleBody(title: "MESSAGE VIEW", body: "SwiftMessages provides a standard message view along with a number of layouts, themes and presentation options.", function: ViewController.demoBasics),
.titleBody(title: "ANY VIEW", body: "Any view, no matter how cute, can be displayed as a message.", function: ViewController.demoAnyView),
.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),
.explore,
.titleBody(title: "CENTERED", body: "Show cenetered messages with a fun, physics-based dismissal gesture.", function: ViewController.demoCentered),
.viewController,
//.counted,
]
/*
MARK: - UITableViewDataSource
*/
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[(indexPath as NSIndexPath).row]
return item.dequeueCell(tableView)
}
/*
MARK: - UITableViewDelegate
*/
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let item = items[(indexPath as NSIndexPath).row]
item.performDemo()
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 50.0
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
/*
MARK: - Demos
*/
static func demoBasics() -> Void {
let error = MessageView.viewFromNib(layout: .tabView)
error.configureTheme(.error)
error.configureContent(title: "Error", body: "Something is horribly wrong!")
error.button?.setTitle("Stop", for: .normal)
let warning = MessageView.viewFromNib(layout: .cardView)
warning.configureTheme(.warning)
warning.configureDropShadow()
let iconText = ["🤔", "😳", "🙄", "😶"].randomElement()!
warning.configureContent(title: "Warning", body: "Consider yourself warned.", iconText: iconText)
warning.button?.isHidden = true
var warningConfig = SwiftMessages.defaultConfig
warningConfig.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
let success = MessageView.viewFromNib(layout: .cardView)
success.configureTheme(.success)
success.configureDropShadow()
success.configureContent(title: "Success", body: "Something good happened!")
success.button?.isHidden = true
var successConfig = SwiftMessages.defaultConfig
successConfig.presentationStyle = .center
successConfig.presentationContext = .window(windowLevel: UIWindow.Level.normal)
let info = MessageView.viewFromNib(layout: .messageView)
info.configureTheme(.info)
info.button?.isHidden = true
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.")
var infoConfig = SwiftMessages.defaultConfig
infoConfig.presentationStyle = .bottom
infoConfig.duration = .seconds(seconds: 0.25)
let status = MessageView.viewFromNib(layout: .statusLine)
status.backgroundView.backgroundColor = UIColor.purple
status.bodyLabel?.textColor = UIColor.white
status.configureContent(body: "A tiny line of text covering the status bar.")
var statusConfig = SwiftMessages.defaultConfig
statusConfig.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
let status2 = MessageView.viewFromNib(layout: .statusLine)
status2.backgroundView.backgroundColor = UIColor.orange
status2.bodyLabel?.textColor = UIColor.white
status2.configureContent(body: "Switched to light status bar!")
var status2Config = SwiftMessages.defaultConfig
status2Config.presentationContext = .window(windowLevel: UIWindow.Level.normal)
status2Config.preferredStatusBarStyle = .lightContent
SwiftMessages.show(view: error)
SwiftMessages.show(config: warningConfig, view: warning)
SwiftMessages.show(config: successConfig, view: success)
SwiftMessages.show(config: infoConfig, view: info)
SwiftMessages.show(config: statusConfig, view: status)
SwiftMessages.show(config: status2Config, view: status2)
}
static func demoAnyView() -> Void {
let imageView = UIImageView()
imageView.image = UIImage(named: "puppies")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let messageView = BaseView(frame: .zero)
messageView.layoutMargins = .zero
messageView.backgroundHeight = 120.0
do {
let backgroundView = CornerRoundingView()
backgroundView.cornerRadius = 15
backgroundView.layer.masksToBounds = true
messageView.installBackgroundView(backgroundView)
messageView.installContentView(imageView)
messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
}
messageView.configureDropShadow()
var config = SwiftMessages.defaultConfig
config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
SwiftMessages.show(config: config, view: messageView)
}
static func demoCustomNib() {
let view: TacoDialogView = try! SwiftMessages.viewFromNib()
view.configureDropShadow()
view.getTacosAction = { _ in SwiftMessages.hide() }
view.cancelAction = { SwiftMessages.hide() }
var config = SwiftMessages.defaultConfig
config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
config.duration = .forever
config.presentationStyle = .bottom
config.dimMode = .gray(interactive: true)
SwiftMessages.show(config: config, view: view)
}
static func demoCentered() {
let messageView: MessageView = MessageView.viewFromNib(layout: .centeredView)
messageView.configureBackgroundView(width: 250)
messageView.configureContent(title: "Hey There!", body: "Please try swiping to dismiss this message.", iconImage: nil, iconText: "🦄", buttonImage: nil, buttonTitle: "No Thanks") { _ in
SwiftMessages.hide()
}
messageView.backgroundView.backgroundColor = UIColor.init(white: 0.97, alpha: 1)
messageView.backgroundView.layer.cornerRadius = 10
var config = SwiftMessages.defaultConfig
config.presentationStyle = .center
config.duration = .forever
config.dimMode = .blur(style: .dark, alpha: 1, interactive: true)
config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar)
SwiftMessages.show(config: config, view: messageView)
}
}
typealias Function = () -> Void
enum Item {
case titleBody(title: String, body: String, function: Function)
case explore
case counted
case viewController
func dequeueCell(_ tableView: UITableView) -> UITableViewCell {
switch self {
case .titleBody(let title, let body, _):
let cell = tableView.dequeueReusableCell(withIdentifier: "TitleBody") as! TitleBodyCell
cell.titleLabel.text = title
cell.bodyLabel.text = body
cell.configureBodyTextStyle()
return cell
case .explore:
let cell = tableView.dequeueReusableCell(withIdentifier: "Explore") as! TitleBodyCell
cell.configureBodyTextStyle()
return cell
case .counted:
let cell = tableView.dequeueReusableCell(withIdentifier: "Counted") as! TitleBodyCell
cell.configureBodyTextStyle()
cell.bodyLabel.configureCodeStyle(on: "show()")
cell.bodyLabel.configureCodeStyle(on: "hideCounted(id:)")
return cell
case .viewController:
let cell = tableView.dequeueReusableCell(withIdentifier: "ViewController") as! TitleBodyCell
cell.configureBodyTextStyle()
return cell
}
}
func performDemo() {
switch self {
case .titleBody(_, _, let function):
function()
default:
break
}
}
}
class TitleBodyCell: UITableViewCell {
@IBOutlet var titleLabel: UILabel!
@IBOutlet var bodyLabel: UILabel!
func configureBodyTextStyle() {
let bodyStyle = NSMutableParagraphStyle()
bodyStyle.lineSpacing = 5.0
bodyLabel.configureBodyTextStyle()
}
}
================================================
FILE: Demo/Demo/ViewControllersViewController.swift
================================================
//
// ViewControllersViewController.swift
// Demo
//
// Created by Timothy Moose on 7/28/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import UIKit
import SwiftMessages
class ViewControllersViewController: UIViewController {
@objc @IBAction private func dismissPresented(segue: UIStoryboardSegue) {
dismiss(animated: true, completion: nil)
}
}
class SwiftMessagesTopSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .topMessage)
}
}
class SwiftMessagesTopCardSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .topCard)
}
}
class SwiftMessagesTopTabSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .topTab)
}
}
class SwiftMessagesBottomSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .bottomMessage)
}
}
class SwiftMessagesBottomCardSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .bottomCard)
}
}
class SwiftMessagesBottomTabSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .bottomTab)
}
}
class SwiftMessagesCenteredSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .centered)
}
}
================================================
FILE: Demo/Demo.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
22652712210F698600310344 /* TacoDialogView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86C0AB9F1D5E814600F76BD6 /* TacoDialogView.xib */; };
226FA5E61F506993004CB2BC /* CountedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226FA5E51F506993004CB2BC /* CountedViewController.swift */; };
226FA5E81F5071D0004CB2BC /* CountedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226FA5E71F5071D0004CB2BC /* CountedMessageView.swift */; };
226FA5EA1F507586004CB2BC /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226FA5E91F507586004CB2BC /* Utils.swift */; };
22F27953210D0FDE00273E7F /* ViewControllersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F27952210D0FDE00273E7F /* ViewControllersViewController.swift */; };
22FE3FA821193CB90017303D /* SwiftMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22FB324121193A3B005C13D9 /* SwiftMessages.framework */; };
22FE3FA921193CB90017303D /* SwiftMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22FB324121193A3B005C13D9 /* SwiftMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
8642F4361D5F7F540061BDCD /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8642F4351D5F7F540061BDCD /* ExploreViewController.swift */; };
86AEDCE61D5D1DB70030232E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AEDCE51D5D1DB70030232E /* AppDelegate.swift */; };
86AEDCE81D5D1DB70030232E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AEDCE71D5D1DB70030232E /* ViewController.swift */; };
86AEDCEB1D5D1DB70030232E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 86AEDCE91D5D1DB70030232E /* Main.storyboard */; };
86AEDCED1D5D1DB70030232E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 86AEDCEC1D5D1DB70030232E /* Assets.xcassets */; };
86AEDCF01D5D1DB70030232E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 86AEDCEE1D5D1DB70030232E /* LaunchScreen.storyboard */; };
86C0ABA21D5E816600F76BD6 /* TacoDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C0ABA11D5E816600F76BD6 /* TacoDialogView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
22FB324021193A3B005C13D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 86B48AEC1D5A41C900063E2B;
remoteInfo = SwiftMessages;
};
22FB324421193A3B005C13D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 86B48AF51D5A41C900063E2B;
remoteInfo = SwiftMessagesTests;
};
22FB324621193A4D005C13D9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 86B48AEB1D5A41C900063E2B;
remoteInfo = SwiftMessages;
};
22FE3FAA21193CB90017303D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 86B48AEB1D5A41C900063E2B;
remoteInfo = SwiftMessages;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
22774C2520B8461A00813732 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
22FE3FB021193CB90017303D /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
22FE3FA921193CB90017303D /* SwiftMessages.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
226FA5E51F506993004CB2BC /* CountedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountedViewController.swift; sourceTree = ""; };
226FA5E71F5071D0004CB2BC /* CountedMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountedMessageView.swift; sourceTree = ""; };
226FA5E91F507586004CB2BC /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
22774C1420B8461900813732 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; };
22F27952210D0FDE00273E7F /* ViewControllersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllersViewController.swift; sourceTree = ""; };
22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftMessages.xcodeproj; path = ../SwiftMessages.xcodeproj; sourceTree = ""; };
8642F4351D5F7F540061BDCD /* ExploreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = ""; };
86AEDCE21D5D1DB70030232E /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
86AEDCE51D5D1DB70030232E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
86AEDCE71D5D1DB70030232E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
86AEDCEA1D5D1DB70030232E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
86AEDCEC1D5D1DB70030232E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
86AEDCEF1D5D1DB70030232E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
86AEDCF11D5D1DB70030232E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
86C0AB9F1D5E814600F76BD6 /* TacoDialogView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TacoDialogView.xib; sourceTree = ""; };
86C0ABA11D5E816600F76BD6 /* TacoDialogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TacoDialogView.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
86AEDCDF1D5D1DB70030232E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
22FE3FA821193CB90017303D /* SwiftMessages.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
22FB323B21193A3B005C13D9 /* Products */ = {
isa = PBXGroup;
children = (
22FB324121193A3B005C13D9 /* SwiftMessages.framework */,
22FB324521193A3B005C13D9 /* SwiftMessagesTests.xctest */,
);
name = Products;
sourceTree = "";
};
6AB98897E817EE976DD72852 /* Frameworks */ = {
isa = PBXGroup;
children = (
22774C1420B8461900813732 /* Messages.framework */,
);
name = Frameworks;
sourceTree = "";
};
86AEDCD91D5D1DB70030232E = {
isa = PBXGroup;
children = (
86AEDCE41D5D1DB70030232E /* Demo */,
86AEDCE31D5D1DB70030232E /* Products */,
6AB98897E817EE976DD72852 /* Frameworks */,
22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */,
);
sourceTree = "";
};
86AEDCE31D5D1DB70030232E /* Products */ = {
isa = PBXGroup;
children = (
86AEDCE21D5D1DB70030232E /* Demo.app */,
);
name = Products;
sourceTree = "";
};
86AEDCE41D5D1DB70030232E /* Demo */ = {
isa = PBXGroup;
children = (
86AEDCE51D5D1DB70030232E /* AppDelegate.swift */,
86AEDCE91D5D1DB70030232E /* Main.storyboard */,
86AEDCE71D5D1DB70030232E /* ViewController.swift */,
8642F4351D5F7F540061BDCD /* ExploreViewController.swift */,
226FA5E51F506993004CB2BC /* CountedViewController.swift */,
22F27952210D0FDE00273E7F /* ViewControllersViewController.swift */,
86AEDCEC1D5D1DB70030232E /* Assets.xcassets */,
86AEDCEE1D5D1DB70030232E /* LaunchScreen.storyboard */,
86AEDCF11D5D1DB70030232E /* Info.plist */,
86C0ABA11D5E816600F76BD6 /* TacoDialogView.swift */,
86C0AB9F1D5E814600F76BD6 /* TacoDialogView.xib */,
226FA5E71F5071D0004CB2BC /* CountedMessageView.swift */,
226FA5E91F507586004CB2BC /* Utils.swift */,
);
path = Demo;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
86AEDCE11D5D1DB70030232E /* Demo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 86AEDCF41D5D1DB70030232E /* Build configuration list for PBXNativeTarget "Demo" */;
buildPhases = (
86AEDCDE1D5D1DB70030232E /* Sources */,
86AEDCDF1D5D1DB70030232E /* Frameworks */,
86AEDCE01D5D1DB70030232E /* Resources */,
22774C2520B8461A00813732 /* Embed App Extensions */,
22FE3FB021193CB90017303D /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
22FB324721193A4D005C13D9 /* PBXTargetDependency */,
22FE3FAB21193CB90017303D /* PBXTargetDependency */,
);
name = Demo;
productName = Demo;
productReference = 86AEDCE21D5D1DB70030232E /* Demo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
86AEDCDA1D5D1DB70030232E /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0940;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "SwiftKick Mobile";
TargetAttributes = {
86AEDCE11D5D1DB70030232E = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1020;
};
};
};
buildConfigurationList = 86AEDCDD1D5D1DB70030232E /* Build configuration list for PBXProject "Demo" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 86AEDCD91D5D1DB70030232E;
productRefGroup = 86AEDCE31D5D1DB70030232E /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 22FB323B21193A3B005C13D9 /* Products */;
ProjectRef = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */;
},
);
projectRoot = "";
targets = (
86AEDCE11D5D1DB70030232E /* Demo */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
22FB324121193A3B005C13D9 /* SwiftMessages.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = SwiftMessages.framework;
remoteRef = 22FB324021193A3B005C13D9 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
22FB324521193A3B005C13D9 /* SwiftMessagesTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = SwiftMessagesTests.xctest;
remoteRef = 22FB324421193A3B005C13D9 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
86AEDCE01D5D1DB70030232E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
86AEDCF01D5D1DB70030232E /* LaunchScreen.storyboard in Resources */,
22652712210F698600310344 /* TacoDialogView.xib in Resources */,
86AEDCED1D5D1DB70030232E /* Assets.xcassets in Resources */,
86AEDCEB1D5D1DB70030232E /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
86AEDCDE1D5D1DB70030232E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8642F4361D5F7F540061BDCD /* ExploreViewController.swift in Sources */,
226FA5E81F5071D0004CB2BC /* CountedMessageView.swift in Sources */,
86AEDCE81D5D1DB70030232E /* ViewController.swift in Sources */,
226FA5EA1F507586004CB2BC /* Utils.swift in Sources */,
86AEDCE61D5D1DB70030232E /* AppDelegate.swift in Sources */,
226FA5E61F506993004CB2BC /* CountedViewController.swift in Sources */,
86C0ABA21D5E816600F76BD6 /* TacoDialogView.swift in Sources */,
22F27953210D0FDE00273E7F /* ViewControllersViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
22FB324721193A4D005C13D9 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = SwiftMessages;
targetProxy = 22FB324621193A4D005C13D9 /* PBXContainerItemProxy */;
};
22FE3FAB21193CB90017303D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = SwiftMessages;
targetProxy = 22FE3FAA21193CB90017303D /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
86AEDCE91D5D1DB70030232E /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
86AEDCEA1D5D1DB70030232E /* Base */,
);
name = Main.storyboard;
sourceTree = "";
};
86AEDCEE1D5D1DB70030232E /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
86AEDCEF1D5D1DB70030232E /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
86AEDCF21D5D1DB70030232E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
};
name = Debug;
};
86AEDCF31D5D1DB70030232E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
86AEDCF51D5D1DB70030232E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Demo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages.Demo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
86AEDCF61D5D1DB70030232E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Demo/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages.Demo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
86AEDCDD1D5D1DB70030232E /* Build configuration list for PBXProject "Demo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
86AEDCF21D5D1DB70030232E /* Debug */,
86AEDCF31D5D1DB70030232E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
86AEDCF41D5D1DB70030232E /* Build configuration list for PBXNativeTarget "Demo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
86AEDCF51D5D1DB70030232E /* Debug */,
86AEDCF61D5D1DB70030232E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 86AEDCDA1D5D1DB70030232E /* Project object */;
}
================================================
FILE: Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
================================================
================================================
FILE: LICENSE.md
================================================
Copyright (c) 2016 SwiftKick Mobile LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "SwiftMessages",
platforms: [
.iOS("13.0")
],
products: [
.library(name: "SwiftMessages", targets: ["SwiftMessages"]),
.library(name: "SwiftMessages-Dynamic", type: .dynamic, targets: ["SwiftMessages"])
],
targets: [
.target(
name: "SwiftMessages",
path: "SwiftMessages",
exclude: [
"Info.plist",
],
resources: [.process("Resources")]
)
]
)
================================================
FILE: README.md
================================================
# SwiftMessages
[](https://twitter.com/TimothyMoose)
[](http://cocoadocs.org/docsets/SwiftMessages)
[](http://cocoadocs.org/docsets/SwiftMessages)
[](http://cocoadocs.org/docsets/SwiftMessages)
[](https://github.com/Carthage/Carthage)
## Overview
🔥🔥🔥 **UPDATE** SwiftUI support in in Xcode 26 requries 10.0.2!
SwiftMessages is a very flexible view and view controller presentation library for UIKit and SwiftUI.
Message views and view controllers can be displayed at the top, bottom, or center of the screen, or behind navigation bars and tab bars. There are interactive dismiss gestures including a fun, physics-based one. Multiple background dimming modes. And a lot more!
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:
* Copy one of the included nib files into your project and change it.
* Subclass `MessageView` and add elements, etc.
* Or just supply an arbitrary instance of `View` or `UIView`.
## Installation
### Swift Package Manager
Go to `File | Swift Packages | Add Package Dependency...` in Xcode and search for "SwiftMessages". If multiple results are found, select the one owned by SwiftKick Mobile.
### CocoaPods
Add the following line to your Podfile:
````ruby
pod 'SwiftMessages'
````
### Carthage
Add the following line to your Cartfile:
````ruby
github "SwiftKickMobile/SwiftMessages"
````
If the Carthage build fails, [try using the script](https://github.com/Carthage/Carthage/issues/3019).
### Manual
1. Put SwiftMessages repo somewhere in your project directory.
1. In Xcode, add `SwiftMessages.xcodeproj` to your project.
1. On your app's target, add the SwiftMessages framework:
1. as an embedded binary on the General tab.
1. as a target dependency on the Build Phases tab.
## Usage
### Basics
````swift
SwiftMessages.show(view: myView)
````
Although you can show any instance of `UIView`, SwiftMessages provides a `MessageView` class
and assortment of nib-based layouts that should handle most cases:
````swift
// Instantiate a message view from the provided card view layout. SwiftMessages searches for nib
// files in the main bundle first, so you can easily copy them into your project and make changes.
let view = MessageView.viewFromNib(layout: .cardView)
// Theme message elements with the warning style.
view.configureTheme(.warning)
// Add a drop shadow.
view.configureDropShadow()
// Set message title, body, and icon. Here, we're overriding the default warning
// image with an emoji character.
let iconText = ["🤔", "😳", "🙄", "😶"].randomElement()!
view.configureContent(title: "Warning", body: "Consider yourself warned.", iconText: iconText)
// Increase the external margin around the card. In general, the effect of this setting
// depends on how the given layout is constrained to the layout margins.
view.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
// Reduce the corner radius (applicable to layouts featuring rounded corners).
(view.backgroundView as? CornerRoundingView)?.cornerRadius = 10
// Show the message.
SwiftMessages.show(view: view)
````
You may wish to use the view provider variant `show(viewProvider:)` to ensure that
your UIKit code is executed on the main queue:
````swift
SwiftMessages.show {
let view = MessageView.viewFromNib(layout: .cardView)
// ... configure the view
return view
}
````
The `SwiftMessages.Config` struct provides numerous configuration options that can be passed to `show()`:
````swift
var config = SwiftMessages.Config()
// Slide up from the bottom.
config.presentationStyle = .bottom
// Display in a window at the specified window level.
config.presentationContext = .window(windowLevel: .statusBar)
Note that, as of iOS 13, it is no longer possible to cover the status bar
regardless of the window level. A workaround is to hide the status bar instead.
config.prefersStatusBarHidden = true
// Disable the default auto-hiding behavior.
config.duration = .forever
// Dim the background like a popover view. Hide when the background is tapped.
config.dimMode = .gray(interactive: true)
// Disable the interactive pan-to-hide gesture.
config.interactiveHide = false
// Specify haptic feedback (see also MessageView/configureTheme)
config.haptic = .success
// Specify a status bar style to if the message is displayed directly under the status bar.
config.preferredStatusBarStyle = .lightContent
// Specify one or more event listeners to respond to show and hide events.
config.eventListeners.append() { event in
if case .didHide = event {
print("yep id=\(String(describing: event.id)")
}
}
SwiftMessages.show(config: config, view: view)
````
Specify default configuration options:
````swift
SwiftMessages.defaultConfig.presentationStyle = .bottom
// Show message with default config.
SwiftMessages.show(view: view)
// Customize config using the default as a base.
var config = SwiftMessages.defaultConfig
config.duration = .forever
SwiftMessages.show(config: config, view: view)
````
### View Controllers
SwiftMessages can present view controllers using the `SwiftMessagesSegue` custom modal segue!
[`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) is a subclass of `UIStoryboardSegue` that integrates directly into Interface Builder as a custom modal segue, enabling view controllers to take advantage of SwiftMessages layouts, animations and more. `SwiftMessagesSegue` works with any UIKIt project — storyboards are not required. Refer to the View Controllers readme below for more information.
#### [View Controllers Readme](./ViewControllers.md)
And check out our blog post [Elegant Custom UIViewController Transitioning](http://www.swiftkickmobile.com/elegant-custom-uiviewcontroller-transitioning-uiviewcontrollertransitioningdelegate-uiviewcontrolleranimatedtransitioning/) to learn a great technique you can use to build your own custom segues that utilize `UIViewControllerTransitioningDelegate` and `UIViewControllerAnimatedTransitioning`.
### SwiftUI
Any of the built-in SwiftMessages views can be displayed by calling the SwiftMessages APIs from within observable object, a button action closure, etc. However, SwiftMessages can also display your custom SwiftUI views.
#### Presentation
Take the following message view and companion data model:
````swift
struct DemoMessage: Identifiable {
let title: String
let body: String
var id: String { title + body }
}
struct DemoMessageView: View {
let message: DemoMessage
var body: some View {
VStack(alignment: .leading) {
Text(message.title).font(.system(size: 20, weight: .bold))
Text(message.body)
}
.multilineTextAlignment(.leading)
.padding(30)
// This makes the message width greedy
.frame(maxWidth: .infinity)
.background(.gray)
// This makes a tab-style view where the bottom corners are rounded and
// the view's background extends to the top edge.
.mask(
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
// This causes the background to extend into the safe area to the screen edge.
.edgesIgnoringSafeArea(.top)
)
}
}
````
You can show it from a button action, view model or other similar context like:
````swift
struct DemoView: View {
var body: some View {
Button("Show message") {
let message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
let messageView = MessageHostingView(id: message.id, content: DemoMessageView(message: message)
SwiftMessages.show(view: messageView)
}
}
}
````
But you may also use a state-based approach using the `swiftMessage()` view modifier:
````swift
struct DemoView: View {
@State var message: DemoMessage?
var body: some View {
Button("Show message") {
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
}
.swiftMessage(message: $message) { message in
DemoMessageView(message: message)
}
}
}
````
This is very similar to the `.sheet()` modifier. However, it doesn't expose all of the features of SwiftMessages, such as explicitly hiding messages by ID. It is totally reasonable to use a combination of both approaches.
If your message views are purely data-driven and don't require delegates, callbacks, etc., there is a slightly simplified variation on `swiftMessage()` that doesn't require a view builder. Instead, your data model should conform to `MessageViewConvertible`.
````swift
extension DemoMessage: MessageViewConvertible {
func asMessageView() -> DemoMessageView {
DemoMessageView(message: self)
}
}
````
Then you can drop the view builder when calling `swiftMessage()`:
````swift
struct DemoView: View {
@State var message: DemoMessage?
var body: some View {
Button("Show message") {
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
}
.swiftMessage(message: $message)
}
}
````
#### Dismissal
SwiftMessages provides several approaches for dismissing messages from within SwiftUI views.
For messages shown using the `swiftMessage()` modifier, you can dismiss by setting the binding to `nil`:
````swift
struct DemoView: View {
@State var message: DemoMessage?
var body: some View {
VStack {
Button("Show message") {
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
}
Button("Hide message") {
message = nil // Dismisses the message
}
}
.swiftMessage(message: $message) { message in
DemoMessageView(message: message)
}
}
}
````
For messages that need to dismiss themselves, SwiftMessages provides a SwiftUI-style environment action:
````swift
struct SelfDismissingMessageView: View {
@Environment(\.swiftMessagesHide) private var hide
let message: DemoMessage
var body: some View {
VStack(alignment: .leading) {
Text(message.title).font(.system(size: 20, weight: .bold))
Text(message.body)
Button("Dismiss") {
hide(animated: true) // Self-dismiss with animation
}
}
.padding(30)
.frame(maxWidth: .infinity)
.background(.gray)
}
}
````
You can also call the SwiftMessages APIs directly from within your SwiftUI views:
````swift
Button("Hide Current") {
SwiftMessages.hide()
}
````
Try it out in the SwiftUI demo app!
### Accessibility
SwiftMessages provides excellent VoiceOver support out-of-the-box.
* The title and body of the message are combined into a single announcement when the message is shown. The `MessageView.accessibilityPrefix` property can be set to prepend additional clarifying text to the announcement.
Sometimes, a message may contain important visual cues that aren't captured in the title or body. For example, a message may rely on a yellow background to convey a warning rather than having the word "warning" in the title or body. In this case, it might be helpful to set `MessageView.accessibilityPrefix = "warning"`.
* If the message is shown with a dim view using `config.dimMode`, elements below the dim view are not focusable until the message is hidden. If `config.dimMode.interactive == true`, the dim view itself will be focusable and read out "dismiss" followed by "button". The former text can be customized by setting the `config.dimModeAccessibilityLabel` property.
See the `AccessibleMessage` protocol for implementing proper accessibility support in custom views.
### Keyboard Avoidance
The `KeyboardTrackingView` class can be used to cause the message view to avoid the keyboard by sliding up when the keyboard gets too close.
````swift
var config = SwiftMessages.defaultConfig
config.keyboardTrackingView = KeyboardTrackingView()
````
You can incorporate `KeyboardTrackingView` into your app even when you're not using SwiftMessages. Install into your view hierarchy by pinning `KeyboardTrackingView` to the bottom, leading, and trailing edges of the screen. Then pin the bottom of your content that should avoid the keyboard to the top `KeyboardTrackingView`. Use an equality constraint to strictly track the keyboard or an inequality constraint to only move when the keyboard gets too close. `KeyboardTrackingView` works by observing keyboard notifications and adjusting its height to maintain its top edge above the keyboard, thereby pushing your content up. See the comments in `KeyboardTrackingView` for configuration options.
### Message Queueing
You can call `SwiftMessages.show()` as many times as you like. SwiftMessages maintains a queue and shows messages one at a time. If your view implements the `Identifiable` protocol (like `MessageView`), duplicate messages will be removed automatically. The pause between messages can be adjusted:
````swift
SwiftMessages.pauseBetweenMessages = 1.0
````
There are a few ways to hide messages programatically:
````swift
// Hide the current message.
SwiftMessages.hide()
// Or hide the current message and clear the queue.
SwiftMessages.hideAll()
// Or for a view that implements `Identifiable`:
SwiftMessages.hide(id: someId)
// Or hide when the number of calls to show() and hideCounted(id:) for a
// given message ID are equal. This can be useful for messages that may be
// shown from multiple code paths to ensure that all paths are ready to hide.
SwiftMessages.hideCounted(id: someId)
````
Multiple instances of `SwiftMessages` can be used to show more than one message at a time. Note that the static `SwiftMessages.show()` and other static APIs on `SwiftMessage` are just convenience wrappers around the shared instance `SwiftMessages.sharedInstance`). Instances must be retained, thus it should be a property of something (e.g. your view controller):
````swift
class SomeViewController: UIViewController {
let otherMessages = SwiftMessages()
func someMethod() {
SwiftMessages.show(...)
otherMessages.show(...)
}
}
````
### Retrieving Messages
There are several APIs available for retrieving messages that are currently being shown, hidden, or queued to be shown. These APIs are useful for updating messages
when some event happens without needing to keep temporary references around.
See also `eventListeners`.
````swift
// Get a message view with the given ID if it is currently
// being shown or hidden.
if let view = SwiftMessages.current(id: "some id") { ... }
// Get a message view with the given ID if is it currently
// queued to be shown.
if let view = SwiftMessages.queued(id: "some id") { ... }
// Get a message view with the given ID if it is currently being
// shown, hidden or in the queue to be shown.
if let view = SwiftMessages.currentOrQueued(id: "some id") { ... }
````
### Customization
SwiftMessages can display any `UIView`. However, there are varying degrees of customization that can be done to the bundled views.
#### Nib Files
All of the message designs bundled with SwiftMessages have associated nib files. You are encouraged to copy any of these nib files into your project and modify them to suit your needs. SwiftMessages will load your copy of the file instead of the original. Nib files may be copied in Xcode using drag-and-drop.
To facilitate the use of nib-based layouts, `MessageView` provides some type-safe convenience methods for loading the bundled nibs:
````swift
let view = MessageView.viewFromNib(layout: .cardView)
````
In addition, the `SwiftMessages` class provides some generic loading methods:
````swift
// Instantiate MessageView from a named nib.
let view: MessageView = try! SwiftMessages.viewFromNib(named: "MyCustomNib")
// Instantiate MyCustomView from a nib named MyCustomView.nib.
let view: MyCustomView = try! SwiftMessages.viewFromNib()
````
#### MessageView Class
[`MessageView`](./SwiftMessages/MessageView.swift) is a light-weight view that all of the bundled designs use. It primarily consists of the following optional `@IBOutlet` properties:
Element | Declaration | Description
--------|-----------|-----
Title | `titleLabel: UILabel?` | The message title.
Message body | `bodyLabel: UILabel?` | The body of the message.
Image icon | `iconImageView: UIImageView?` | An image-based icon.
Text icon | `iconLabel: UILabel?` | A text-based (emoji) alternative to the image icon.
Button | `button: UIButton?` | An action button.
The SwiftMessages nib file use `MessageView` as the top-level view with content connected to these outlets. The layouts are done using stack views, which means that you can remove an element by simply hiding it:
````swift
view.titleLabel.isHidden = true
````
A common mistake is attempting to remove an element by setting the corresponding outlet to `nil`. This does not work because it does not remove the element from the view hierarchy.
#### Configuration
`MessageView` provides numerous methods that follow the `configure*` naming convention:
````swift
view.configureTheme(.warning, includeHaptic: true)
view.configureContent(title: "Warning", body: "Consider yourself warned.", iconText: "🤔")
````
All of these methods are shortcuts for quickly configuring the underlying view properties. SwiftMessages strives to avoid doing any internal magic in these methods, so you do not need to call them. You can configure the view properties directly or combine the two approaches.
#### Interaction
`MessageView` provides an optional block-based tap handler for the button and another for the view itself:
````swift
// Hide when button tapped
messageView.buttonTapHandler = { _ in SwiftMessages.hide() }
// Hide when message view tapped
messageView.tapHandler = { _ in SwiftMessages.hide() }
````
#### Extending
The suggested method for starting with `MessageView` as a base and __adding new elements__, such as additional buttons, is as follows:
1. Copy one of the bundled nib files into your project or create a new one from scratch.
1. Add new elements to the nib file.
1. Sublcass `MessageView` and create outlets for the new elements.
1. Assign the top-level view in the nib file to the subclass.
1. Connect outlets between the nib file and the subclass.
1. (recommended) override the implementation of `Identifiable` as needed to incorporate new elements into the message's identity.
1. (recommended) override the implementation of `AccessibleMessage` as needed to incorporate new elements into Voice Over.
1. Use one of the nib-loading methods above to load the view.
#### BaseView Class
[`BaseView`](./SwiftMessages/BaseView.swift) is the superclass of `MessageView` and provides numerous options that aren't specific to the "title + body + icon + button" design of `MessageView`. Custom views that are significantly different from `MessageView`, such as a progress indicator, should subclass `BaseView`.
#### CornerRoundingView Class
[`CornerRoundingView`](./SwiftMessages/CornerRoundingView.swift) is a custom view that messages can use for rounding all or a subset of corners with squircles (the smoother method of rounding corners that you see on app icons). The nib files that feature rounded corners have `backgroundView` assigned to a `CornerRoundingView`. It provides a `roundsLeadingCorners` option to dynamically round only the leading corners of the view when presented from top or bottom (a feature used for the tab-style layouts).
#### Animator Protocol
[`Animator`](./SwiftMessages/Animator.swift) is the protocol that SwiftMessages uses for presentation and dismissal animations. Custom animations can be done through the `SwiftMessages.PresentationStyle.custom(animator:)`. Some related components:
* [`TopBottomAnimation`](./SwiftMessages/TopBottomAnimation.swift) is a sliding implementation of `Animator` used internally by `.top` and `.bottom` presentation styles. It provides some customization options.
* [`PhysicsAnimation`](./SwiftMessages/PhysicsAnimation.swift) is a scaling + opacity implementation of `Animator` used internally by the `.center` presentation style. It provides a fun physics-based dismissal gesture and provides customization options including `.top` and `.bottom` placement.
* [`PhysicsPanHandler`](./SwiftMessages/PhysicsPanHandler.swift) provides the physics-based dismissal gesture for `PhysicsAnimation` and can be incorporated into other `Animator` implementations.
High-quality PRs for cool `Animator` implementations are welcome!
#### MarginAdjustable Protocol
[`MarginAdjustable`](./SwiftMessages/MarginAdjustable.swift) is a protocol adopted by `BaseView`. If the view being presented adopts `MarginAdjustable`, SwiftMessages takes ownership of the view's layout margins to ensure ideal spacing across the full range of presentation contexts.
#### BackgroundViewable Protocol
[`BackgroundViewable`](./SwiftMessages/BackgroundViewable.swift) is a protocol adopted by `BaseView` and requires that a view provide a single `backgroundView` property. `BaseView` initializes `backgroundView = self`, which you can freely re-assign to any subview.
If the view being presented adopts `BackgroundViewable`, SwiftMessages will ignore touches outside of `backgroundView`. This is important because message views always span the full width of the device. Card and tab-style layouts appear inset from the edges of the device because the message view's background is transparent and `backgroundView` is assigned to a subview constrained to the layout margins. In these layouts, touches in the transparent margins should be ignored.
#### Identifiable Protocol
[`Identifiable`](./SwiftMessages/Identifiable.swift) is a protocol adopted by `MessageView` and requires that a view provide a single `id` property, which SwiftMessages uses for message deduplication.
`MessageView` computes the `id` based on the message content, but `id` can also be set explicitly as needed.
#### AccessibleMessage Protocol
[`AccessibleMessage`](./SwiftMessages/AccessibleMessage.swift) is a protocol adopted by `MessageView`. If the view being presented adopts `AccessibleMessage`, SwiftMessages provides improved Voice Over.
## About SwiftKick Mobile
We build high quality apps! [Get in touch](http://www.swiftkickmobile.com) if you need help with a project.
## License
SwiftMessages is distributed under the MIT license. [See LICENSE](./LICENSE.md) for details.
================================================
FILE: SwiftMessages/AccessibleMessage.swift
================================================
//
// AccessibleMessage.swift
// SwiftMessages
//
// Created by Timothy Moose on 3/11/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import Foundation
/**
Message views that conform to `AccessibleMessage` will have proper accessibility behavior when displaying messages.
`MessageView` implements this protocol.
*/
public protocol AccessibleMessage {
var accessibilityMessage: String? { get }
var accessibilityElement: NSObject? { get }
var additionalAccessibilityElements: [NSObject]? { get }
}
================================================
FILE: SwiftMessages/Animator.swift
================================================
//
// Animator.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/4/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
public typealias AnimationCompletion = (_ completed: Bool) -> Void
@MainActor
public protocol AnimationDelegate: AnyObject {
func hide(animator: Animator)
func panStarted(animator: Animator)
func panEnded(animator: Animator)
}
/**
An option set representing the known types of safe area conflicts
that could require margin adjustments on the message view in order to
get the layouts to look right.
*/
public struct SafeZoneConflicts: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
/// Message view behind status bar
public static let statusBar = SafeZoneConflicts(rawValue: 1 << 0)
/// Message view behind the sensor notch on iPhone X
public static let sensorNotch = SafeZoneConflicts(rawValue: 1 << 1)
/// Message view behind home indicator on iPhone X
public static let homeIndicator = SafeZoneConflicts(rawValue: 1 << 2)
/// Message view is over the status bar on an iPhone 8 or lower. This is a special
/// case because we logically expect the top safe area to be zero, but it is reported as 20
/// (which seems like an iOS bug). We use the `overStatusBar` to indicate this special case.
public static let overStatusBar = SafeZoneConflicts(rawValue: 1 << 3)
}
public class AnimationContext {
public let messageView: UIView
public let containerView: UIView
public let safeZoneConflicts: SafeZoneConflicts
public let interactiveHide: Bool
init(messageView: UIView, containerView: UIView, safeZoneConflicts: SafeZoneConflicts, interactiveHide: Bool) {
self.messageView = messageView
self.containerView = containerView
self.safeZoneConflicts = safeZoneConflicts
self.interactiveHide = interactiveHide
}
}
@MainActor
public protocol Animator: AnyObject {
/// Adopting classes should declare as `weak`.
var delegate: AnimationDelegate? { get set }
func show(context: AnimationContext, completion: @escaping AnimationCompletion)
func hide(context: AnimationContext, completion: @escaping AnimationCompletion)
/// The show animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`,
/// then provide an estimate. This value is used by `SwiftMessagesSegue`.
var showDuration: TimeInterval { get }
/// The hide animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`,
/// then provide an estimate. This value is used by `SwiftMessagesSegue`.
var hideDuration: TimeInterval { get }
}
================================================
FILE: SwiftMessages/BackgroundViewable.swift
================================================
//
// BackgroundViewable.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/15/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/**
Message views that implement the `BackgroundViewable` protocol will have the
pan-to-hide gesture recognizer installed in the `backgroundView`. Message views
always span the full width of the containing view. Typically, the `backgroundView`
property defines the message view's visible region, allowing for card-style views
where the message view background is transparent and the background view is inset
from by some amount. See CardView.nib, for example.
This protocol is optional. Message views that don't implement `BackgroundViewable`
will have the pan-to-hide gesture installed in the message view itself.
*/
public protocol BackgroundViewable {
var backgroundView: UIView! { get }
}
================================================
FILE: SwiftMessages/BaseView.swift
================================================
//
// BaseView.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/17/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/**
The `BaseView` class is a reusable message view base class that implements some
of the optional SwiftMessages protocols and provides some convenience functions
and a configurable tap handler. Message views do not need to inherit from `BaseVew`.
*/
open class BaseView: UIView, BackgroundViewable, MarginAdjustable {
/*
MARK: - IB outlets
*/
/**
Fulfills the `BackgroundViewable` protocol and is the target for
the optional `tapHandler` block. Defaults to `self`.
*/
@IBOutlet open weak var backgroundView: UIView! {
didSet {
if let old = oldValue {
old.removeGestureRecognizer(tapRecognizer)
}
installTapRecognizer()
updateBackgroundHeightConstraint()
}
}
// The `contentView` property was removed because it no longer had any functionality
// in the framework. This is a minor backwards incompatible change. If you've copied
// one of the included nib files from a previous release, you may get a key-value
// coding runtime error related to contentView, in which case you can subclass the
// view and add a `contentView` property or you can remove the outlet connection in
// Interface Builder.
// @IBOutlet public var contentView: UIView!
/*
MARK: - Initialization
*/
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundView = self
layoutMargins = UIEdgeInsets.zero
}
public override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = self
layoutMargins = UIEdgeInsets.zero
}
/*
MARK: - Installing background and content
*/
/**
A convenience function for installing a content view as a subview of `backgroundView`
and pinning the edges to `backgroundView` with the specified `insets`.
- Parameter contentView: The view to be installed into the background view
and assigned to the `contentView` property.
- Parameter insets: The amount to inset the content view from the background view.
Default is zero inset.
*/
open func installContentView(_ contentView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) {
contentView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: insets.top).isActive = true
contentView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -insets.bottom).isActive = true
contentView.leftAnchor.constraint(equalTo: backgroundView.leftAnchor, constant: insets.left).isActive = true
contentView.rightAnchor.constraint(equalTo: backgroundView.rightAnchor, constant: -insets.right).isActive = true
contentView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true
}
/**
A convenience function for installing a background view and pinning to the layout margins.
This is useful for creating programatic layouts where the background view needs to be
inset from the message view's edges (like a card-style layout).
- Parameter backgroundView: The view to be installed as a subview and
assigned to the `backgroundView` property.
- Parameter insets: The amount to inset the content view from the margins. Default is zero inset.
*/
open func installBackgroundView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) {
backgroundView.translatesAutoresizingMaskIntoConstraints = false
if backgroundView != self {
backgroundView.removeFromSuperview()
}
addSubview(backgroundView)
self.backgroundView = backgroundView
backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true
backgroundView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 900)).isActive = true
backgroundView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 900)).isActive = true
backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true
layoutConstraints = [
backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)),
]
regularWidthLayoutConstraints = [
backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)),
backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)),
]
installTapRecognizer()
}
/**
A convenience function for installing a background view and pinning to the horizontal
layout margins and to the vertical edges. This is useful for creating programatic layouts where
the background view needs to be inset from the message view's horizontal edges (like a tab-style layout).
- Parameter backgroundView: The view to be installed as a subview and
assigned to the `backgroundView` property.
- Parameter insets: The amount to inset the content view from the horizontal margins and vertical edges.
Default is zero inset.
*/
open func installBackgroundVerticalView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) {
backgroundView.translatesAutoresizingMaskIntoConstraints = false
if backgroundView != self {
backgroundView.removeFromSuperview()
}
addSubview(backgroundView)
self.backgroundView = backgroundView
backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true
backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true
backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true
layoutConstraints = [
backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)),
]
regularWidthLayoutConstraints = [
backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)),
backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)),
backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)),
]
installTapRecognizer()
}
/*
MARK: - Tap handler
*/
/**
An optional tap handler that will be called when the `backgroundView` is tapped.
*/
open var tapHandler: ((_ view: BaseView) -> Void)? {
didSet {
installTapRecognizer()
}
}
fileprivate lazy var tapRecognizer: UITapGestureRecognizer = {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(MessageView.tapped))
return tapRecognizer
}()
@objc func tapped() {
tapHandler?(self)
}
fileprivate func installTapRecognizer() {
guard let backgroundView = backgroundView else { return }
removeGestureRecognizer(tapRecognizer)
backgroundView.removeGestureRecognizer(tapRecognizer)
if tapHandler != nil {
// Only install the tap recognizer if there is a tap handler,
// which makes it slightly nicer if one wants to install
// a custom gesture recognizer.
backgroundView.addGestureRecognizer(tapRecognizer)
}
}
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if backgroundView != self {
let backgroundViewPoint = convert(point, to: backgroundView)
return backgroundView.point(inside: backgroundViewPoint, with: event)
}
return super.point(inside: point, with: event)
}
/*
MARK: - MarginAdjustable
These properties fulfill the `MarginAdjustable` protocol and are exposed
as `@IBInspectables` so that they can be adjusted directly in nib files
(see MessageView.nib).
*/
public var layoutMarginAdditions: UIEdgeInsets {
get {
return UIEdgeInsets(top: topLayoutMarginAddition, left: leftLayoutMarginAddition, bottom: bottomLayoutMarginAddition, right: rightLayoutMarginAddition)
}
set {
topLayoutMarginAddition = newValue.top
leftLayoutMarginAddition = newValue.left
bottomLayoutMarginAddition = newValue.bottom
rightLayoutMarginAddition = newValue.right
}
}
/// Start margins from the safe area.
open var respectSafeArea: Bool = true
/// IBInspectable access to layoutMarginAdditions.top
@IBInspectable open var topLayoutMarginAddition: CGFloat = 0
/// IBInspectable access to layoutMarginAdditions.left
@IBInspectable open var leftLayoutMarginAddition: CGFloat = 0
/// IBInspectable access to layoutMarginAdditions.bottom
@IBInspectable open var bottomLayoutMarginAddition: CGFloat = 0
/// IBInspectable access to layoutMarginAdditions.right
@IBInspectable open var rightLayoutMarginAddition: CGFloat = 0
@IBInspectable open var collapseLayoutMarginAdditions: Bool = true
@IBInspectable open var bounceAnimationOffset: CGFloat = 5
/*
MARK: - Setting the height
*/
/**
An optional explicit height for the background view, which can be used if
the message view's intrinsic content size does not produce the desired height.
*/
open var backgroundHeight: CGFloat? {
didSet {
updateBackgroundHeightConstraint()
}
}
private func updateBackgroundHeightConstraint() {
if let existing = backgroundHeightConstraint {
let view = existing.firstItem as! UIView
view.removeConstraint(existing)
backgroundHeightConstraint = nil
}
if let height = backgroundHeight, let backgroundView = backgroundView {
let constraint = NSLayoutConstraint(item: backgroundView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height)
backgroundView.addConstraint(constraint)
backgroundHeightConstraint = constraint
}
}
private var backgroundHeightConstraint: NSLayoutConstraint?
/*
Mark: - Layout
*/
open override func updateConstraints() {
super.updateConstraints()
let on: [NSLayoutConstraint]
let off: [NSLayoutConstraint]
switch traitCollection.horizontalSizeClass {
case .regular:
on = regularWidthLayoutConstraints
off = layoutConstraints
default:
on = layoutConstraints
off = regularWidthLayoutConstraints
}
on.forEach { $0.isActive = true }
off.forEach { $0.isActive = false }
}
private var layoutConstraints: [NSLayoutConstraint] = []
private var regularWidthLayoutConstraints: [NSLayoutConstraint] = []
open override func layoutSubviews() {
super.layoutSubviews()
updateShadowPath()
}
}
/*
MARK: - Theming
*/
extension BaseView {
/// A convenience function to configure a default drop shadow effect.
/// The shadow is to this view's layer instead of that of the background view
/// because the background view may be masked. So, when modifying the drop shadow,
/// be sure to set the shadow properties of this view's layer. The shadow path is
/// updated for you automatically.
public func configureDropShadow() {
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
layer.shadowRadius = 6.0
layer.shadowOpacity = 0.4
layer.masksToBounds = false
updateShadowPath()
}
/// A convenience function to turn off drop shadow
public func configureNoDropShadow() {
layer.shadowOpacity = 0
}
private func updateShadowPath() {
backgroundView?.layoutIfNeeded()
let shadowLayer = backgroundView?.layer ?? layer
let shadowRect = layer.convert(shadowLayer.bounds, from: shadowLayer)
let shadowPath: CGPath?
if let backgroundMaskLayer = shadowLayer.mask as? CAShapeLayer,
let backgroundMaskPath = backgroundMaskLayer.path {
var transform = CGAffineTransform(translationX: shadowRect.minX, y: shadowRect.minY)
shadowPath = backgroundMaskPath.copy(using: &transform)
} else {
shadowPath = UIBezierPath(roundedRect: shadowRect, cornerRadius: shadowLayer.cornerRadius).cgPath
}
// This is a workaround needed for smooth rotation animations.
if let foundAnimation = layer.findAnimation(forKeyPath: "bounds.size") {
// Update the layer's `shadowPath` with animation, copying the relevant properties
// from the found animation.
let animation = CABasicAnimation(keyPath: "shadowPath")
animation.duration = foundAnimation.duration
animation.timingFunction = foundAnimation.timingFunction
animation.fromValue = layer.shadowPath
animation.toValue = shadowPath
layer.add(animation, forKey: "shadowPath")
layer.shadowPath = shadowPath
} else {
// Update the layer's `shadowPath` without animation
layer.shadowPath = shadowPath }
}
}
/*
MARK: - Configuring the width
This extension provides a few convenience functions for configuring the
background view's width. You are encouraged to write your own such functions
if these don't exactly meet your needs.
*/
extension BaseView {
/**
A shortcut for configuring the left and right layout margins. For views that
have `backgroundView` as a subview of `MessageView`, the background view should
be pinned to the left and right `layoutMargins` in order for this configuration to work.
*/
public func configureBackgroundView(sideMargin: CGFloat) {
layoutMargins.left = sideMargin
layoutMargins.right = sideMargin
}
/**
A shortcut for adding a width constraint to the `backgroundView`. When calling this
method, it is important to ensure that the width constraint doesn't conflict with
other constraints. The CardView.nib and TabView.nib layouts are compatible with
this method.
*/
public func configureBackgroundView(width: CGFloat) {
guard let backgroundView = backgroundView else { return }
let constraint = NSLayoutConstraint(item: backgroundView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: width)
backgroundView.addConstraint(constraint)
}
}
================================================
FILE: SwiftMessages/CALayer+Extensions.swift
================================================
//
// CALayer+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/3/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import QuartzCore
extension CALayer {
func findAnimation(forKeyPath keyPath: String) -> CABasicAnimation? {
return animationKeys()?
.compactMap({ animation(forKey: $0) as? CABasicAnimation })
.filter({ $0.keyPath == keyPath })
.first
}
}
================================================
FILE: SwiftMessages/CornerRoundingView.swift
================================================
//
// CornerRoundingView.swift
// SwiftMessages
//
// Created by Timothy Moose on 7/28/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import UIKit
/// A background view that messages can use for rounding all or a subset of corners with squircles
/// (the smoother method of rounding corners that you see on app icons).
open class CornerRoundingView: UIView {
/// Specifies the corner radius to use.
@IBInspectable
open var cornerRadius: CGFloat = 0 {
didSet {
updateMaskPath()
}
}
/// Set to `true` for layouts where only the leading corners should be
/// rounded. For example, the layout in TabView.xib rounds the bottom corners
/// when displayed from the top and the top corners when displayed from the bottom.
/// When this property is `true`, the `roundedCorners` property will be overwritten
/// by relevant animators (e.g. `TopBottomAnimation`).
@IBInspectable
open var roundsLeadingCorners: Bool = false
/// Specifies which corners should be rounded. When `roundsLeadingCorners = true`, relevant
/// relevant animators (e.g. `TopBottomAnimation`) will overwrite the value of this property.
open var roundedCorners: UIRectCorner = [.allCorners] {
didSet {
updateMaskPath()
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
private func sharedInit() {
layer.mask = shapeLayer
}
private let shapeLayer = CAShapeLayer()
override open func layoutSubviews() {
super.layoutSubviews()
updateMaskPath()
}
private func updateMaskPath() {
let newPath = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: roundedCorners, cornerRadii: cornerRadii).cgPath
// Update the `shapeLayer's` path with animation if we detect our `layer's` size is being animated.
// This is a workaround needed for smooth rotation animations.
if let foundAnimation = layer.findAnimation(forKeyPath: "bounds.size") {
// Update the `shapeLayer's` path with animation, copying the relevant properties
// from the found animation.
let animation = CABasicAnimation(keyPath: "path")
animation.duration = foundAnimation.duration
animation.timingFunction = foundAnimation.timingFunction
animation.fromValue = shapeLayer.path
animation.toValue = newPath
shapeLayer.add(animation, forKey: "path")
shapeLayer.path = newPath
} else {
// Update the `shapeLayer's` path without animation
shapeLayer.path = newPath
}
}
private var cornerRadii: CGSize {
return CGSize(width: cornerRadius, height: cornerRadius)
}
}
================================================
FILE: SwiftMessages/Error.swift
================================================
//
// Error.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/7/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import Foundation
/**
The `SwiftMessagesError` enum contains the errors thrown by SwiftMessages.
*/
enum SwiftMessagesError: Error {
case cannotLoadViewFromNib(nibName: String)
case noRootViewController
}
================================================
FILE: SwiftMessages/HapticMessage.swift
================================================
//
// HapticMessage.swift
// SwiftMessages
//
// Created by Timothy Moose on 1/23/24.
// Copyright © 2024 SwiftKick Mobile. All rights reserved.
//
import Foundation
/**
Message views that conform to `HapticMessage` can specify a haptic feedback to be used when presented.
*/
protocol HapticMessage {
var defaultHaptic: SwiftMessages.Haptic? { get }
}
================================================
FILE: SwiftMessages/Identifiable.swift
================================================
//
// Identifiable.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/1/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import Foundation
/**
Message views that adopt the `Identifiable` protocol will have duplicate messages
removed from the `MessageView` queue. Typically, the `id` would be set to a string
representation of the content of the message view. For example, `MessageView`, combines
the title and message body text.
This protocol is optional. Message views that don't adopt `Identifiable` will not
have duplicates removed.
*/
public protocol Identifiable {
var id: String { get }
}
================================================
FILE: SwiftMessages/Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
================================================
FILE: SwiftMessages/KeyboardTrackingView.swift
================================================
//
// KeyboardTrackingView.swift
// SwiftMessages
//
// Created by Timothy Moose on 5/20/19.
// Copyright © 2019 SwiftKick Mobile. All rights reserved.
//
import UIKit
public protocol KeyboardTrackingViewDelegate: AnyObject {
func keyboardTrackingViewWillChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any])
func keyboardTrackingViewDidChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any])
}
/// A view that adjusts it's height based on keyboard hide and show notifications.
/// Pin it to the bottom of the screen using Auto Layout and then pin views that
/// should avoid the keyboard to the top of it. Supply an instance of this class
/// on `SwiftMessages.Config.keyboardTrackingView` or `SwiftMessagesSegue.keyboardTrackingView`
/// for automatic keyboard avoidance for the entire SwiftMessages view or view controller.
open class KeyboardTrackingView: UIView {
public enum Change {
case show
case hide
case frame
}
public weak var delegate: KeyboardTrackingViewDelegate?
/// Typically, when a view controller is not being displayed, keyboard
/// tracking should be paused to avoid responding to keyboard events
/// caused by other view controllers or apps. Setting `isPaused = false` in
/// `viewWillAppear` and `isPaused = true` in `viewWillDisappear` usually works. This class
/// automatically pauses and resumes when the app resigns and becomes active, respectively.
open var isPaused = false {
didSet {
if !isPaused {
isAutomaticallyPaused = false
}
}
}
/// The margin to maintain between the keyboard and the top of the view.
@IBInspectable open var topMargin: CGFloat = 0
/// Subclasses can override this to do something before the change.
open func willChange(
change: KeyboardTrackingView.Change,
userInfo: [AnyHashable : Any]
) {}
/// Subclasses can override this to do something after the change.
open func didChange(
change: KeyboardTrackingView.Change,
userInfo: [AnyHashable : Any]
) {}
override public init(frame: CGRect) {
super.init(frame: frame)
postInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
open override func awakeFromNib() {
super.awakeFromNib()
postInit()
}
private var isAutomaticallyPaused = false
private var heightConstraint: NSLayoutConstraint!
private var lastObservedKeyboardRect: CGRect?
private func postInit() {
translatesAutoresizingMaskIntoConstraints = false
heightConstraint = heightAnchor.constraint(equalToConstant: 0)
heightConstraint.isActive = true
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.didBecomeActiveNotification, object: nil)
backgroundColor = .clear
}
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
show(change: .frame, notification)
}
@objc private func keyboardWillShow(_ notification: Notification) {
show(change: .show, notification)
}
@objc private func keyboardWillHide(_ notification: Notification) {
guard !(isPaused || isAutomaticallyPaused),
let userInfo = (notification as NSNotification).userInfo else { return }
guard heightConstraint.constant != 0 else { return }
delegate?.keyboardTrackingViewWillChange(change: .hide, userInfo: userInfo)
animateKeyboardChange(change: .hide, height: 0, userInfo: userInfo)
}
@objc private func pause() {
isAutomaticallyPaused = true
}
@objc private func resume() {
isAutomaticallyPaused = false
}
open override func layoutSubviews() {
super.layoutSubviews()
heightConstraint.constant = calculateHeightConstant()
}
private func show(change: Change, _ notification: Notification) {
guard !(isPaused || isAutomaticallyPaused),
let userInfo = (notification as NSNotification).userInfo,
let value = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
willChange(change: change, userInfo: userInfo)
delegate?.keyboardTrackingViewWillChange(change: change, userInfo: userInfo)
lastObservedKeyboardRect = value.cgRectValue
let newHeight = calculateHeightConstant()
guard heightConstraint.constant != newHeight else { return }
animateKeyboardChange(change: change, height: newHeight, userInfo: userInfo)
}
private func animateKeyboardChange(change: Change, height: CGFloat, userInfo: [AnyHashable: Any]) {
if let durationNumber = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber {
UIView.animate(withDuration: durationNumber.doubleValue, delay: 0, options: .curveEaseInOut, animations: {
self.heightConstraint.constant = height
self.updateConstraintsIfNeeded()
self.superview?.layoutIfNeeded()
}) { completed in
self.didChange(change: change, userInfo: userInfo)
self.delegate?.keyboardTrackingViewDidChange(change: change, userInfo: userInfo)
}
}
}
private func calculateHeightConstant() -> CGFloat {
guard let keyboardRect = lastObservedKeyboardRect else { return 0 }
let thisRect = convert(bounds, to: nil)
return max(0, thisRect.maxY - keyboardRect.minY) + topMargin
}
}
================================================
FILE: SwiftMessages/MarginAdjustable+Extensions.swift
================================================
//
// MarginAdjustable+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 11/5/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension MarginAdjustable where Self: UIView {
public func defaultMarginAdjustment(context: AnimationContext) -> UIEdgeInsets {
var layoutMargins: UIEdgeInsets = layoutMarginAdditions
var safeAreaInsets: UIEdgeInsets = {
guard respectSafeArea else { return .zero }
insetsLayoutMarginsFromSafeArea = false
return self.safeAreaInsets
}()
if !context.safeZoneConflicts.isDisjoint(with: .overStatusBar) {
safeAreaInsets.top = 0
}
layoutMargins = collapseLayoutMarginAdditions
? layoutMargins.collapse(toInsets: safeAreaInsets)
: layoutMargins + safeAreaInsets
return layoutMargins
}
}
extension UIEdgeInsets {
func collapse(toInsets insets: UIEdgeInsets) -> UIEdgeInsets {
let top = self.top.collapse(toInset: insets.top)
let left = self.left.collapse(toInset: insets.left)
let bottom = self.bottom.collapse(toInset: insets.bottom)
let right = self.right.collapse(toInset: insets.right)
return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
}
}
extension CGFloat {
func collapse(toInset inset: CGFloat) -> CGFloat {
return Swift.max(self, inset)
}
}
================================================
FILE: SwiftMessages/MarginAdjustable.swift
================================================
//
// MarginAdjustable.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/5/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/*
Message views that implement the `MarginAdjustable` protocol will have their
`layoutMargins` adjusted by SwiftMessages to account for the height of the
status bar (when displayed under the status bar) and a small amount of
overshoot in the bounce animation. `MessageView` implements this protocol
by way of its parent class `BaseView`.
For the effect of this protocol to work, subviews should be pinned to the
message view's margins and the `layoutMargins` property should not be modified.
This protocol is optional. A message view that doesn't implement `MarginAdjustable`
is responsible for setting is own internal margins appropriately.
*/
public protocol MarginAdjustable {
/// The amount to add to the safe area insets in calculating
/// the layout margins.
var layoutMarginAdditions: UIEdgeInsets { get }
/// When `true`, SwiftMessages automatically collapses layout margin additions (topLayoutMarginAddition, etc.)
/// when the default layout margins are greater than zero. This is typically used when a margin addition is only
/// needed when the safe area inset is zero for a given edge. When the safe area inset for a given edge is non-zero,
/// the additional margin is not added.
var collapseLayoutMarginAdditions: Bool { get set }
/// Start margins from the safe area.
var respectSafeArea: Bool { get set }
var bounceAnimationOffset: CGFloat { get set }
}
================================================
FILE: SwiftMessages/MaskingView.swift
================================================
//
// MaskingView.swift
// SwiftMessages
//
// Created by Timothy Moose on 3/11/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
class MaskingView: PassthroughView {
func install(keyboardTrackingView: KeyboardTrackingView) {
self.keyboardTrackingView = keyboardTrackingView
keyboardTrackingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(keyboardTrackingView)
keyboardTrackingView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
keyboardTrackingView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
keyboardTrackingView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
var accessibleElements: [NSObject] = []
weak var backgroundView: UIView? {
didSet {
oldValue?.removeFromSuperview()
if let view = backgroundView {
view.isUserInteractionEnabled = false
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
sendSubviewToBack(view)
}
}
}
override func accessibilityElementCount() -> Int {
return accessibleElements.count
}
override func accessibilityElement(at index: Int) -> Any? {
return accessibleElements[index]
}
override func index(ofAccessibilityElement element: Any) -> Int {
guard let object = element as? NSObject else { return 0 }
return accessibleElements.firstIndex(of: object) ?? 0
}
init() {
super.init(frame: CGRect.zero)
clipsToBounds = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
clipsToBounds = true
}
private var keyboardTrackingView: KeyboardTrackingView?
override func addSubview(_ view: UIView) {
super.addSubview(view)
guard let keyboardTrackingView = keyboardTrackingView,
view != keyboardTrackingView,
view != backgroundView else { return }
let offset: CGFloat
if let adjustable = view as? MarginAdjustable {
offset = -adjustable.bounceAnimationOffset
} else {
offset = 0
}
keyboardTrackingView.topAnchor.constraint(
greaterThanOrEqualTo: view.bottomAnchor,
constant: offset
).with(priority: UILayoutPriority(250)).isActive = true
}
}
================================================
FILE: SwiftMessages/MessageGeometryProxy.swift
================================================
//
// MessageGeometryProxy.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/24/24.
// Copyright © 2024 SwiftKick Mobile. All rights reserved.
//
import SwiftUI
/// A data type that mimicks `GeomtryProxy` and is used with `swiftMessage()` modifier when the geomtry metrics of the container view
/// are needed, particularly because `GeometryReader` doesn't work inside the view builder due to the way the message view is being
/// displayed from UIKit.
public struct MessageGeometryProxy {
public var size: CGSize
public var safeAreaInsets: EdgeInsets
}
================================================
FILE: SwiftMessages/MessageHostingView.swift
================================================
//
// MessageHostingView.swift
// SwiftMessages
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
import UIKit
/// A rudimentary hosting view for SwiftUI messages.
@available(iOS 14.0, *)
public class MessageHostingView: UIView, Identifiable where Content: View {
// MARK: - API
public let id: String
public init(id: String, content: Content) {
self.id = id
self.content = { _ in content }
super.init(frame: .zero)
backgroundColor = .clear
}
public init(
message: Message,
@ViewBuilder content: @escaping (Message, MessageGeometryProxy) -> Content
) where Message: Identifiable {
self.id = message.id
self.content = { geom in content(message, geom) }
super.init(frame: .zero)
backgroundColor = .clear
}
convenience public init(message: Message) where Message: MessageViewConvertible, Message.Content == Content {
self.init(id: message.id, content: message.asMessageView() )
}
// MARK: - Constants
// MARK: - Variables
private var hostVC: UIHostingController?
private let content: (MessageGeometryProxy) -> Content
// MARK: - Lifecycle
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Override hit testing so that only SwiftUI-rendered content inside `MessageHostingView` can receive touches.
///
/// Background:
/// - `MessageHostingView` does not tightly wrap its SwiftUI content, potentially leaving surrounding regions that should not be tappable. There have
/// been some complications with detecting touches on the SwiftUI content over the years that have led to the current approach:
/// - On iOS 18, UIKit performs a second hit test that resolves to the `UIHostingController`'s view instead of the actual SwiftUI element.
/// - On iOS 26, the `UIHostingController`'s view no longer contains any subviews, but its `CALayer` *layer* hierarchy still reflects the SwiftUI content.
///
/// All of these issues can be solved by hit testing the layer hierarchy instead of the view hierarchy:
/// - Call `super.hitTest(point, with: event)` to obtain a candidate view `view`. If our heuristic determines that SwiftUI content was tapped,
/// then we return `view` to accept the touch. Otherwise, return `nil` to pass the touch through.
/// - If the candidate is `MessageHostingView` return `nil`.
/// - If the candidate is directly parented to `MessageHostingView`, this is the `UIHostingController` view containing the SwiftUI content.
/// To determine if SwiftUI content was touched, we iterate over hosting controller's sublayers and return the candidate if the touch intersects a sublayer.
/// Otherwise, return `nil`.
/// - For any other case, we return the candidate because we don't know what's going on.
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let view = super.hitTest(point, with: event) else { return nil }
if view == self { return nil }
if view.superview == self {
for sublayer in view.layer.sublayers ?? [] {
let sublayerPoint = self.layer.convert(point, to: sublayer)
if sublayer.contains(sublayerPoint) {
return view
}
}
return nil
}
return view
}
public override func didMoveToSuperview() {
guard let superview = self.superview else { return }
let size = superview.bounds.size
let insets = superview.safeAreaInsets
let ltr = superview.effectiveUserInterfaceLayoutDirection == .leftToRight
let proxy = MessageGeometryProxy(
size: CGSize(
width: size.width - insets.left - insets.right,
height: size.height - insets.top - insets.bottom
),
safeAreaInsets: EdgeInsets(
top: insets.top,
leading: ltr ? insets.left : insets.right,
bottom: insets.bottom,
trailing: ltr ? insets.right : insets.left
)
)
let hostVC = UIHostingController(rootView: content(proxy))
self.hostVC = hostVC
hostVC.loadViewIfNeeded()
installContentView(hostVC.view)
hostVC.view.backgroundColor = .clear
}
// MARK: - Configuration
private func installContentView(_ contentView: UIView) {
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
])
}
}
================================================
FILE: SwiftMessages/MessageView.swift
================================================
//
// MessageView.swift
// SwiftMessages
//
// Created by Timothy Moose on 7/30/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/*
*/
open class MessageView: BaseView, Identifiable, AccessibleMessage, HapticMessage {
/*
MARK: - Haptic feedback
*/
/// The default haptic feedback to be used when the message is presented.
open var defaultHaptic: SwiftMessages.Haptic?
/*
MARK: - Button tap handler
*/
/// An optional button tap handler. The `button` is automatically
/// configured to call this tap handler on `.TouchUpInside`.
open var buttonTapHandler: ((_ button: UIButton) -> Void)?
@objc func buttonTapped(_ button: UIButton) {
buttonTapHandler?(button)
}
/*
MARK: - Touch handling
*/
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Only accept touches within the background view. Anything outside of the
// background view's bounds should be transparent and does not need to receive
// touches. This helps with tap dismissal when using `DimMode.gray` and `DimMode.color`.
return backgroundView == self
? super.point(inside: point, with: event)
: backgroundView.point(inside: convert(point, to: backgroundView), with: event)
}
/*
MARK: - IB outlets
*/
/// An optional title label.
@IBOutlet open var titleLabel: UILabel?
/// An optional body text label.
@IBOutlet open var bodyLabel: UILabel?
/// An optional icon image view.
@IBOutlet open var iconImageView: UIImageView?
/// An optional icon label (e.g. for emoji character, icon font, etc.).
@IBOutlet open var iconLabel: UILabel?
/// An optional button. This buttons' `.TouchUpInside` event will automatically
/// invoke the optional `buttonTapHandler`, but its fine to add other target
/// action handlers can be added.
@IBOutlet open var button: UIButton? {
didSet {
if let old = oldValue {
old.removeTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
}
if let button = button {
button.addTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside)
}
}
}
/*
MARK: - Identifiable
*/
open var id: String {
get {
return customId ?? "MessageView:title=\(String(describing: titleLabel?.text)), body=\(String(describing: bodyLabel?.text))"
}
set {
customId = newValue
}
}
private var customId: String?
/*
MARK: - AccessibleMessage
*/
/**
An optional prefix for the `accessibilityMessage` that can
be used to further clarify the message for VoiceOver. For example,
the view's background color or icon might convey that a message is
a warning, in which case one may specify the value "warning".
*/
open var accessibilityPrefix: String?
open var accessibilityMessage: String? {
#if swift(>=4.1)
let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].compactMap { $0 }
#else
let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].flatMap { $0 }
#endif
guard components.count > 0 else { return nil }
return components.joined(separator: ", ")
}
public var accessibilityElement: NSObject? {
return backgroundView
}
open var additionalAccessibilityElements: [NSObject]? {
var elements: [NSObject] = []
func getAccessibleSubviews(view: UIView) {
for subview in view.subviews {
if subview.isAccessibilityElement {
elements.append(subview)
} else {
// Only doing this for non-accessible `subviews`, which avoids
// including button labels, etc.
getAccessibleSubviews(view: subview)
}
}
}
getAccessibleSubviews(view: self.backgroundView)
return elements
}
}
/*
MARK: - Creating message views
This extension provides several convenience functions for instantiating
`MessageView` from the included nib files in a type-safe way. These nib
files can be found in the Resources folder and can be drag-and-dropped
into a project and modified. You may still use these APIs if you've
copied the nib files because SwiftMessages looks for them in the main
bundle first. See `SwiftMessages` for additional nib loading options.
*/
extension MessageView {
/**
Specifies one of the nib files included in the Resources folders.
*/
public enum Layout: String {
/**
The standard message view that stretches across the full width of the
container view.
*/
case messageView = "MessageView"
/**
A floating card-style view with rounded corners.
*/
case cardView = "CardView"
/**
Like `CardView` with one end attached to the super view.
*/
case tabView = "TabView"
/**
A 20pt tall view that can be used to overlay the status bar.
Note that this layout will automatically grow taller if displayed
directly under the status bar (see the `ContentInsetting` protocol).
*/
case statusLine = "StatusLine"
/**
A floating card-style view with elements centered and arranged vertically.
This view is typically used with `.center` presentation style.
*/
case centeredView = "CenteredView"
}
/**
Loads the nib file associated with the given `Layout` and returns the first
view found in the nib file with the matching type `T: MessageView`.
- Parameter layout: The `Layout` option to use.
- Parameter filesOwner: An optional files owner.
- Returns: An instance of generic view type `T: MessageView`.
*/
public static func viewFromNib(layout: Layout, filesOwner: AnyObject = NSNull.init()) -> T {
return try! SwiftMessages.viewFromNib(named: layout.rawValue)
}
/**
Loads the nib file associated with the given `Layout` from
the given bundle and returns the first view found in the nib
file with the matching type `T: MessageView`.
- Parameter layout: The `Layout` option to use.
- Parameter bundle: The name of the bundle containing the nib file.
- Parameter filesOwner: An optional files owner.
- Returns: An instance of generic view type `T: MessageView`.
*/
public static func viewFromNib(layout: Layout, bundle: Bundle, filesOwner: AnyObject = NSNull.init()) -> T {
return try! SwiftMessages.viewFromNib(named: layout.rawValue, bundle: bundle, filesOwner: filesOwner)
}
}
/*
MARK: - Layout adjustments
This extension provides a few convenience functions for adjusting the layout.
*/
extension MessageView {
/**
Constrains the image view to a specified size. By default, the size of the
image view is determined by its `intrinsicContentSize`.
- Parameter size: The size to be translated into Auto Layout constraints.
- Parameter contentMode: The optional content mode to apply.
*/
public func configureIcon(withSize size: CGSize, contentMode: UIView.ContentMode? = nil) {
var views: [UIView] = []
if let iconImageView = iconImageView { views.append(iconImageView) }
if let iconLabel = iconLabel { views.append(iconLabel) }
views.forEach {
let constraints = [$0.heightAnchor.constraint(equalToConstant: size.height),
$0.widthAnchor.constraint(equalToConstant: size.width)]
constraints.forEach { $0.priority = UILayoutPriority(999.0) }
$0.addConstraints(constraints)
if let contentMode = contentMode {
$0.contentMode = contentMode
}
}
}
}
/*
MARK: - Theming
This extension provides a few convenience functions for setting styles,
colors and icons. You are encouraged to write your own such functions
if these don't exactly meet your needs.
*/
extension MessageView {
/**
A convenience function for setting some pre-defined colors and icons.
- Parameter theme: The theme type to use.
- Parameter iconStyle: The icon style to use. Defaults to `.Default`.
- Parameter useHaptics: If `true`, configures an appropriate haptic based on theme. Defaults to `false`.
*/
public func configureTheme(_ theme: Theme, iconStyle: IconStyle = .default, includeHaptic: Bool = false) {
let iconImage = iconStyle.image(theme: theme)
let backgroundColor: UIColor
let foregroundColor: UIColor
let defaultBackgroundColor: UIColor
switch theme {
case .info:
defaultBackgroundColor = UIColor(red: 225.0/255.0, green: 225.0/255.0, blue: 225.0/255.0, alpha: 1.0)
case .success:
defaultBackgroundColor = UIColor(red: 97.0/255.0, green: 161.0/255.0, blue: 23.0/255.0, alpha: 1.0)
case .warning:
defaultBackgroundColor = UIColor(red: 246.0/255.0, green: 197.0/255.0, blue: 44.0/255.0, alpha: 1.0)
case .error:
defaultBackgroundColor = UIColor(red: 249.0/255.0, green: 66.0/255.0, blue: 47.0/255.0, alpha: 1.0)
}
if includeHaptic {
switch theme {
case .success, .info:
defaultHaptic = SwiftMessages.Haptic.success
case .warning:
defaultHaptic = SwiftMessages.Haptic.warning
case .error:
defaultHaptic = SwiftMessages.Haptic.error
}
}
switch theme {
case .info:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .label
case .success:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 55/255.0, green: 122/255.0, blue: 0/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
case .warning:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 239/255.0, green: 184/255.0, blue: 10/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
case .error:
backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark, .unspecified: return UIColor(red: 195/255.0, green: 12/255.0, blue: 12/255.0, alpha: 1.0)
case .light: fallthrough
@unknown default:
return defaultBackgroundColor
}
}
foregroundColor = .white
}
configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage)
}
/**
A convenience function for setting a foreground and background color.
Note that images will only display the foreground color if they're
configured with UIImageRenderingMode.AlwaysTemplate.
- Parameter backgroundColor: The background color to use.
- Parameter foregroundColor: The foreground color to use.
*/
public func configureTheme(backgroundColor: UIColor, foregroundColor: UIColor, iconImage: UIImage? = nil, iconText: String? = nil) {
iconImageView?.image = iconImage
iconLabel?.text = iconText
iconImageView?.tintColor = foregroundColor
let backgroundView = self.backgroundView ?? self
backgroundView.backgroundColor = backgroundColor
iconLabel?.textColor = foregroundColor
titleLabel?.textColor = foregroundColor
bodyLabel?.textColor = foregroundColor
button?.backgroundColor = foregroundColor
button?.tintColor = backgroundColor
button?.contentEdgeInsets = UIEdgeInsets(top: 7.0, left: 7.0, bottom: 7.0, right: 7.0)
button?.layer.cornerRadius = 5.0
iconImageView?.isHidden = iconImageView?.image == nil
iconLabel?.isHidden = iconLabel?.text == nil
}
}
/*
MARK: - Configuring the content
This extension provides a few convenience functions for configuring the
message content. You are encouraged to write your own such functions
if these don't exactly meet your needs.
SwiftMessages does not try to be clever by adjusting the layout based on
what content you configure. All message elements are optional and it is
up to you to hide or remove elements you don't need. The easiest way to
remove unwanted elements is to drag-and-drop one of the included nib
files into your project as a starting point and make changes.
*/
extension MessageView {
/**
Sets the message body text.
- Parameter body: The message body text to use.
*/
public func configureContent(body: String) {
bodyLabel?.text = body
}
/**
Sets the message title and body text.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
*/
public func configureContent(title: String, body: String) {
configureContent(body: body)
titleLabel?.text = title
}
/**
Sets the message title, body text and icon image. Also hides the
`iconLabel`.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconImage: The icon image to use.
*/
public func configureContent(title: String, body: String, iconImage: UIImage) {
configureContent(title: title, body: body)
iconImageView?.image = iconImage
iconImageView?.isHidden = false
iconLabel?.text = nil
iconLabel?.isHidden = true
}
/**
Sets the message title, body text and icon text (e.g. an emoji).
Also hides the `iconImageView`.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconText: The icon text to use (e.g. an emoji).
*/
public func configureContent(title: String, body: String, iconText: String) {
configureContent(title: title, body: body)
iconLabel?.text = iconText
iconLabel?.isHidden = false
iconImageView?.isHidden = true
iconImageView?.image = nil
}
/**
Sets all configurable elements.
- Parameter title: The message title to use.
- Parameter body: The message body text to use.
- Parameter iconImage: The icon image to use.
- Parameter iconText: The icon text to use (e.g. an emoji).
- Parameter buttonImage: The button image to use.
- Parameter buttonTitle: The button title to use.
- Parameter buttonTapHandler: The button tap handler block to use.
*/
public func configureContent(title: String?, body: String?, iconImage: UIImage?, iconText: String?, buttonImage: UIImage?, buttonTitle: String?, buttonTapHandler: ((_ button: UIButton) -> Void)?) {
titleLabel?.text = title
bodyLabel?.text = body
iconImageView?.image = iconImage
iconLabel?.text = iconText
button?.setImage(buttonImage, for: .normal)
button?.setTitle(buttonTitle, for: .normal)
self.buttonTapHandler = buttonTapHandler
iconImageView?.isHidden = iconImageView?.image == nil
iconLabel?.isHidden = iconLabel?.text == nil
}
}
================================================
FILE: SwiftMessages/MessageViewConvertible.swift
================================================
//
// MessageViewConvertible.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
@available(iOS 14.0, *)
/// A protocol used to display a SwiftUI message view using the `swiftMessage()` modifier.
public protocol MessageViewConvertible: Equatable, Identifiable {
associatedtype Content: View
func asMessageView() -> Content
}
================================================
FILE: SwiftMessages/NSBundle+Extensions.swift
================================================
//
// NSBundle+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/8/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import Foundation
private class BundleToken {}
extension Bundle {
// This is copied method from SPM generated Bundle.module for CocoaPods support
static func sm_frameworkBundle() -> Bundle {
let candidates = [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: BundleToken.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
]
let bundleNames = [
// For Swift Package Manager
"SwiftMessages_SwiftMessages",
// For Carthage
"SwiftMessages",
]
for bundleName in bundleNames {
for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
}
// Return whatever bundle this code is in as a last resort.
return Bundle(for: BundleToken.self)
}
}
================================================
FILE: SwiftMessages/NSLayoutConstraint+Extensions.swift
================================================
//
// NSLayoutConstraint+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 5/18/19.
// Copyright © 2019 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension NSLayoutConstraint {
func with(priority: UILayoutPriority) -> NSLayoutConstraint {
self.priority = priority
return self
}
}
================================================
FILE: SwiftMessages/PassthroughView.swift
================================================
//
// PassthroughView.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/5/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
class PassthroughView: UIControl {
var tappedHandler: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
initCommon()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initCommon()
}
private func initCommon() {
addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
@objc func tapped() {
tappedHandler?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self && tappedHandler == nil ? nil : view
}
}
================================================
FILE: SwiftMessages/PassthroughWindow.swift
================================================
//
// PassthroughWindow.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/5/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
class PassthroughWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// iOS has started embedding the SwiftMessages view in private views that block
// interaction with views underneath, essentially making the window behave like a modal.
// To work around this, we'll ignore hit test results on these views.
let view = super.hitTest(point, with: event)
if let view = view,
let hitTestView = hitTestView,
hitTestView.isDescendant(of: view) && hitTestView != view {
return nil
}
return view
}
init(hitTestView: UIView) {
self.hitTestView = hitTestView
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private weak var hitTestView: UIView?
}
================================================
FILE: SwiftMessages/PhysicsAnimation.swift
================================================
//
// PhysicsAnimation.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/14/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
@MainActor
public class PhysicsAnimation: NSObject, Animator {
public enum Placement {
case top
case center
case bottom
}
open var placement: Placement = .center
open var showDuration: TimeInterval = 0.5
open var hideDuration: TimeInterval = 0.15
public var panHandler = PhysicsPanHandler()
public weak var delegate: AnimationDelegate?
weak var messageView: UIView?
weak var containerView: UIView?
var context: AnimationContext?
public override init() {}
init(delegate: AnimationDelegate) {
self.delegate = delegate
}
public func show(context: AnimationContext, completion: @escaping AnimationCompletion) {
NotificationCenter.default.addObserver(
self,
selector: #selector(adjustMargins),
name: UIDevice.orientationDidChangeNotification,
object: nil
)
install(context: context)
showAnimation(context: context, completion: completion)
}
public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) {
NotificationCenter.default.removeObserver(self)
if panHandler.isOffScreen {
context.messageView.alpha = 0
panHandler.state?.stop()
}
let view = context.messageView
self.context = context
CATransaction.begin()
CATransaction.setCompletionBlock {
view.alpha = 1
view.transform = CGAffineTransform.identity
completion(true)
}
UIView.animate(
withDuration: hideDuration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction],
animations: {
view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
},
completion: nil
)
UIView.animate(
withDuration: hideDuration,
delay: 0,
options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction],
animations: {
view.alpha = 0
},
completion: nil
)
CATransaction.commit()
}
func install(context: AnimationContext) {
let view = context.messageView
let container = context.containerView
messageView = view
containerView = container
self.context = context
view.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(view)
switch placement {
case .center:
view.centerYAnchor.constraint(
equalTo: container.centerYAnchor
)
.with(priority: UILayoutPriority(200))
.isActive = true
case .top:
view.topAnchor.constraint(
equalTo: container.topAnchor
)
.with(priority: UILayoutPriority(200))
.isActive = true
case .bottom:
view.bottomAnchor.constraint(
equalTo: container.bottomAnchor
)
.with(priority: UILayoutPriority(200))
.isActive = true
}
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
// Don't allow the message to spill outside of the top or bottom of the container.
view.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
view.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor),
])
// Important to layout now in order to get the right safe area insets
container.layoutIfNeeded()
adjustMargins()
container.layoutIfNeeded()
installInteractive(context: context)
}
@objc public func adjustMargins() {
guard let adjustable = messageView as? MarginAdjustable & UIView,
let context = context else { return }
adjustable.preservesSuperviewLayoutMargins = false
adjustable.insetsLayoutMarginsFromSafeArea = false
adjustable.layoutMargins = adjustable.defaultMarginAdjustment(context: context)
}
func showAnimation(context: AnimationContext, completion: @escaping AnimationCompletion) {
let view = context.messageView
view.alpha = 0.25
view.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
CATransaction.begin()
CATransaction.setCompletionBlock {
completion(true)
}
UIView.animate(withDuration: showDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
view.transform = CGAffineTransform.identity
}, completion: nil)
UIView.animate(withDuration: 0.3 * showDuration, delay: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
view.alpha = 1
}, completion: nil)
CATransaction.commit()
}
func installInteractive(context: AnimationContext) {
guard context.interactiveHide else { return }
panHandler.configure(context: context, animator: self)
}
}
================================================
FILE: SwiftMessages/PhysicsPanHandler.swift
================================================
//
// PhysicsPanHandler.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/25/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
@MainActor
open class PhysicsPanHandler {
public var hideDelay: TimeInterval = 0.2
public struct MotionSnapshot {
var angle: CGFloat
var time: CFAbsoluteTime
}
@MainActor
public final class State {
weak var messageView: UIView?
weak var containerView: UIView?
var dynamicAnimator: UIDynamicAnimator
var itemBehavior: UIDynamicItemBehavior
var attachmentBehavior: UIAttachmentBehavior? {
didSet {
if let oldValue = oldValue {
dynamicAnimator.removeBehavior(oldValue)
}
if let attachmentBehavior = attachmentBehavior {
dynamicAnimator.addBehavior(attachmentBehavior)
addSnapshot()
}
}
}
var snapshots: [MotionSnapshot] = []
public init(messageView: UIView, containerView: UIView) {
self.messageView = messageView
self.containerView = containerView
let dynamicAnimator = UIDynamicAnimator(referenceView: containerView)
let itemBehavior = UIDynamicItemBehavior(items: [messageView])
itemBehavior.allowsRotation = true
dynamicAnimator.addBehavior(itemBehavior)
self.itemBehavior = itemBehavior
self.dynamicAnimator = dynamicAnimator
}
func update(attachmentAnchorPoint anchorPoint: CGPoint) {
addSnapshot()
attachmentBehavior?.anchorPoint = anchorPoint
}
func addSnapshot() {
let angle = messageView?.angle ?? snapshots.last?.angle ?? 0
let time = CFAbsoluteTimeGetCurrent()
snapshots.append(MotionSnapshot(angle: angle, time: time))
}
public func stop() {
guard let messageView = messageView else {
dynamicAnimator.removeAllBehaviors()
return
}
let center = messageView.center
let transform = messageView.transform
dynamicAnimator.removeAllBehaviors()
messageView.center = center
messageView.transform = transform
}
public var angularVelocity: CGFloat {
guard let last = snapshots.last else { return 0 }
for previous in snapshots.reversed() {
// Ignore snapshots where the angle or time hasn't changed to avoid degenerate cases.
if previous.angle != last.angle && previous.time != last.time {
return (last.angle - previous.angle) / CGFloat(last.time - previous.time)
}
}
return 0
}
}
weak var animator: Animator?
weak var messageView: UIView?
weak var containerView: UIView?
private(set) public var state: State?
private(set) public var isOffScreen = false
private var restingCenter: CGPoint?
public init() {}
public private(set) lazy var pan: UIPanGestureRecognizer = {
let pan = UIPanGestureRecognizer()
pan.addTarget(self, action: #selector(pan(_:)))
return pan
}()
public func configure(context: AnimationContext, animator: Animator) {
if let oldView = (messageView as? BackgroundViewable)?.backgroundView ?? messageView {
oldView.removeGestureRecognizer(pan)
}
messageView = context.messageView
let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView
view?.addGestureRecognizer(pan)
containerView = context.containerView
self.animator = animator
}
@objc func pan(_ pan: UIPanGestureRecognizer) {
guard let messageView = messageView, let containerView = containerView, let animator = animator else { return }
let anchorPoint = pan.location(in: containerView)
switch pan.state {
case .began:
animator.delegate?.panStarted(animator: animator)
let state = State(messageView: messageView, containerView: containerView)
self.state = state
let center = messageView.center
restingCenter = center
let offset = UIOffset(horizontal: anchorPoint.x - center.x, vertical: anchorPoint.y - center.y)
let attachmentBehavior = UIAttachmentBehavior(item: messageView, offsetFromCenter: offset, attachedToAnchor: anchorPoint)
state.attachmentBehavior = attachmentBehavior
state.itemBehavior.action = { [weak self, weak messageView, weak containerView] in
guard let self = self, !self.isOffScreen, let messageView = messageView, let containerView = containerView, let animator = self.animator else { return }
let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView
let frame = containerView.convert(view.bounds, from: view)
if !containerView.bounds.intersects(frame) {
self.isOffScreen = true
Task {
try? await Task.sleep(seconds: self.hideDelay)
animator.delegate?.hide(animator: animator)
}
}
}
case .changed:
guard let state = state else { return }
state.update(attachmentAnchorPoint: anchorPoint)
case .ended, .cancelled:
guard let state = state else { return }
state.update(attachmentAnchorPoint: anchorPoint)
let velocity = pan.velocity(in: containerView)
let angularVelocity = state.angularVelocity
let speed = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2))
// The multiplier on angular velocity was determined by hand-tuning
let energy = sqrt(pow(speed, 2) + pow(angularVelocity * 75, 2))
if energy > 200 && speed > 600 {
// Limit the speed and angular velocity to reasonable values
let speedScale = speed > 0 ? min(1, 1800 / speed) : 1
let escapeVelocity = CGPoint(x: velocity.x * speedScale, y: velocity.y * speedScale)
let angularSpeedScale = min(1, 10 / abs(angularVelocity))
let escapeAngularVelocity = angularVelocity * angularSpeedScale
state.itemBehavior.addLinearVelocity(escapeVelocity, for: messageView)
state.itemBehavior.addAngularVelocity(escapeAngularVelocity, for: messageView)
state.attachmentBehavior = nil
} else {
state.stop()
self.state = nil
animator.delegate?.panEnded(animator: animator)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0, options: .beginFromCurrentState, animations: {
messageView.center = self.restingCenter ?? CGPoint(x: containerView.bounds.width / 2, y: containerView.bounds.height / 2)
messageView.transform = CGAffineTransform.identity
}, completion: nil)
}
default:
break
}
}
}
extension UIView {
var angle: CGFloat {
// http://stackoverflow.com/a/2051861/1271826
return atan2(transform.b, transform.a)
}
}
================================================
FILE: SwiftMessages/Presenter.swift
================================================
//
// MessagePresenter.swift
// SwiftMessages
//
// Created by Timothy Moose on 7/30/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
@MainActor
protocol PresenterDelegate: AnimationDelegate {
func hide(presenter: Presenter)
}
@MainActor
class Presenter: NSObject {
// MARK: - API
init(config: SwiftMessages.Config, view: UIView, delegate: PresenterDelegate) {
self.config = config
self.view = view
self.delegate = delegate
self.animator = Presenter.animator(forPresentationStyle: config.presentationStyle, delegate: delegate)
if let identifiable = view as? Identifiable {
id = identifiable.id
} else {
var mutableView = view
id = withUnsafePointer(to: &mutableView) { "\($0)" }
}
super.init()
}
var id: String
var config: SwiftMessages.Config
let maskingView = MaskingView()
let animator: Animator
var isHiding = false
let view: UIView
var delayShow: TimeInterval? {
if case .indefinite(let delay, _) = config.duration { return delay }
return nil
}
var showDate: CFTimeInterval?
/// Returns the required delay for hiding based on time shown
var delayHide: TimeInterval? {
if interactivelyHidden { return 0 }
if case .indefinite(_, let minimum) = config.duration, let showDate = showDate {
let timeIntervalShown = CACurrentMediaTime() - showDate
return max(0, minimum - timeIntervalShown)
}
return nil
}
var pauseDuration: TimeInterval? {
let duration: TimeInterval?
switch self.config.duration {
case .automatic:
duration = 2
case .seconds(let seconds):
duration = seconds
case .forever, .indefinite:
duration = nil
}
return duration
}
/// Detects the scenario where the view was shown, but the containing view heirarchy was removed before the view
/// was hidden. This unusual scenario could result in the message queue being blocked because the presented
/// view was not properly hidden by SwiftMessages. `isOrphaned` allows the queuing logic to unblock the queue.
var isOrphaned: Bool {
return installed && view.window == nil
}
// MARK: - Constants
@MainActor
enum PresentationContext {
case viewController(_: Weak)
case view(_: Weak)
func viewControllerValue() -> UIViewController? {
switch self {
case .viewController(let weak):
return weak.value
case .view:
return nil
}
}
func viewValue() -> UIView? {
switch self {
case .viewController(let weak):
return weak.value?.view
case .view(let weak):
return weak.value
}
}
}
// MARK: - Variables
private weak var delegate: PresenterDelegate?
private var presentationContext = PresentationContext.viewController(Weak(value: nil))
private var installed = false
private var interactivelyHidden = false;
// MARK: - Showing and hiding
private static func animator(forPresentationStyle style: SwiftMessages.PresentationStyle, delegate: AnimationDelegate) -> Animator {
switch style {
case .top:
return TopBottomAnimation(style: .top, delegate: delegate)
case .bottom:
return TopBottomAnimation(style: .bottom, delegate: delegate)
case .center:
return PhysicsAnimation(delegate: delegate)
case .custom(let animator):
animator.delegate = delegate
return animator
}
}
func show(completion: @escaping AnimationCompletion) throws {
try presentationContext = getPresentationContext()
install()
self.config.eventListeners.forEach { $0(.willShow(self.view)) }
switch (self.view as? HapticMessage)?.defaultHaptic ?? config.haptic {
case .error?: UINotificationFeedbackGenerator().notificationOccurred(.error)
case .warning?: UINotificationFeedbackGenerator().notificationOccurred(.warning)
case .success?: UINotificationFeedbackGenerator().notificationOccurred(.success)
default: break
}
showAnimation() { completed in
completion(completed)
if completed {
if self.config.dimMode.modal {
self.showAccessibilityFocus()
} else {
self.showAccessibilityAnnouncement()
}
self.config.eventListeners.forEach { $0(.didShow(self.view)) }
}
}
}
private func showAnimation(completion: @escaping AnimationCompletion) {
func dim(_ color: UIColor) {
self.maskingView.backgroundColor = UIColor.clear
UIView.animate(withDuration: 0.2, animations: {
self.maskingView.backgroundColor = color
})
}
func blur(style: UIBlurEffect.Style, alpha: CGFloat) {
let blurView = UIVisualEffectView(effect: nil)
blurView.alpha = alpha
maskingView.backgroundView = blurView
UIView.animate(withDuration: 0.3) {
blurView.effect = UIBlurEffect(style: style)
}
}
let context = animationContext()
animator.show(context: context) { (completed) in
completion(completed)
}
switch config.dimMode {
case .none:
break
case .gray:
dim(UIColor(white: 0, alpha: 0.3))
case .color(let color, _):
dim(color)
case .blur(let style, let alpha, _):
blur(style: style, alpha: alpha)
}
}
private func showAccessibilityAnnouncement() {
guard let accessibleMessage = view as? AccessibleMessage,
let message = accessibleMessage.accessibilityMessage else { return }
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: message)
}
private func showAccessibilityFocus() {
guard let accessibleMessage = view as? AccessibleMessage,
let focus = accessibleMessage.accessibilityElement ?? accessibleMessage.additionalAccessibilityElements?.first else { return }
UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: focus)
}
func hide(animated: Bool, completion: @escaping AnimationCompletion) {
isHiding = true
self.config.eventListeners.forEach { $0(.willHide(self.view)) }
let context = animationContext()
let action = {
if let viewController = self.presentationContext.viewControllerValue() as? WindowViewController {
viewController.uninstall()
}
self.maskingView.removeFromSuperview()
completion(true)
self.config.eventListeners.forEach { $0(.didHide(self.view)) }
}
guard animated else {
action()
return
}
animator.hide(context: context) { (completed) in
action()
}
func undim() {
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: {
self.maskingView.backgroundColor = UIColor.clear
}, completion: nil)
}
func unblur() {
guard let view = maskingView.backgroundView as? UIVisualEffectView else { return }
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: {
view.effect = nil
}, completion: nil)
}
switch config.dimMode {
case .none:
break
case .gray:
undim()
case .color:
undim()
case .blur:
unblur()
}
}
private func animationContext() -> AnimationContext {
return AnimationContext(messageView: view, containerView: maskingView, safeZoneConflicts: safeZoneConflicts(), interactiveHide: config.interactiveHide)
}
private func safeZoneConflicts() -> SafeZoneConflicts {
guard let _ = maskingView.window else { return [] }
let windowLevel: UIWindow.Level = {
if let vc = presentationContext.viewControllerValue() as? WindowViewController {
return vc.config.windowLevel ?? .normal
}
return .normal
}()
// TODO `underNavigationBar` and `underTabBar` should look up the presentation context's hierarchy
// TODO for cases where both should be true (probably not an issue for typical height messages, though).
let underNavigationBar: Bool = {
if let vc = presentationContext.viewControllerValue() as? UINavigationController { return vc.sm_isVisible(view: vc.navigationBar) }
return false
}()
let underTabBar: Bool = {
if let vc = presentationContext.viewControllerValue() as? UITabBarController { return vc.sm_isVisible(view: vc.tabBar) }
return false
}()
if windowLevel > .normal {
// TODO seeing `maskingView.safeAreaInsets.top` value of 20 on
// iPhone 8 with status bar window level. This seems like an iOS bug since
// the message view's window is above the status bar. Applying a special rule
// to allow the animator to revove this amount from the layout margins if needed.
// This may need to be reworked if any future device has a legitimate 20pt top safe area,
// such as with a potentially smaller notch.
if maskingView.safeAreaInsets.top == 20 {
return [.overStatusBar]
} else {
var conflicts: SafeZoneConflicts = []
if maskingView.safeAreaInsets.top > 0 {
conflicts.formUnion(.sensorNotch)
}
if maskingView.safeAreaInsets.bottom > 0 {
conflicts.formUnion(.homeIndicator)
}
return conflicts
}
}
var conflicts: SafeZoneConflicts = []
if !underNavigationBar {
conflicts.formUnion(.sensorNotch)
}
if !underTabBar {
conflicts.formUnion(.homeIndicator)
}
return conflicts
}
private func getPresentationContext() throws -> PresentationContext {
func newWindowViewController() -> UIViewController {
let viewController = WindowViewController.newInstance(config: config)
return viewController
}
switch config.presentationContext {
case .automatic:
#if SWIFTMESSAGES_APP_EXTENSIONS
throw SwiftMessagesError.noRootViewController
#else
if let rootViewController = UIWindow.keyWindow?.rootViewController {
let viewController = rootViewController.sm_selectPresentationContextTopDown(config)
return .viewController(Weak(value: viewController))
} else {
throw SwiftMessagesError.noRootViewController
}
#endif
case .window:
let viewController = newWindowViewController()
return .viewController(Weak(value: viewController))
case .windowScene:
let viewController = newWindowViewController()
return .viewController(Weak(value: viewController))
case .viewController(let viewController):
let viewController = viewController.sm_selectPresentationContextBottomUp(config)
return .viewController(Weak(value: viewController))
case .view(let view):
return .view(Weak(value: view))
}
}
/*
MARK: - Installation
*/
func install() {
func topLayoutConstraint(view: UIView, containerView: UIView, viewController: UIViewController?) -> NSLayoutConstraint {
if case .top = config.presentationStyle.topBottomStyle, let nav = viewController as? UINavigationController, nav.sm_isVisible(view: nav.navigationBar) {
return NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: nav.navigationBar, attribute: .bottom, multiplier: 1.00, constant: 0.0)
}
return NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1.00, constant: 0.0)
}
func bottomLayoutConstraint(view: UIView, containerView: UIView, viewController: UIViewController?) -> NSLayoutConstraint {
if case .bottom = config.presentationStyle.topBottomStyle, let tab = viewController as? UITabBarController, tab.sm_isVisible(view: tab.tabBar), tab.tabBar.superview != nil, !tab.tabBar.frame.isEmpty {
return NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: tab.tabBar, attribute: .top, multiplier: 1.00, constant: 0.0)
}
return NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1.00, constant: 0.0)
}
func installMaskingView(containerView: UIView) {
maskingView.translatesAutoresizingMaskIntoConstraints = false
if let nav = presentationContext.viewControllerValue() as? UINavigationController {
containerView.insertSubview(maskingView, belowSubview: nav.navigationBar)
} else if let tab = presentationContext.viewControllerValue() as? UITabBarController {
containerView.insertSubview(maskingView, belowSubview: tab.tabBar)
} else {
containerView.addSubview(maskingView)
}
maskingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
maskingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
topLayoutConstraint(view: maskingView, containerView: containerView, viewController: presentationContext.viewControllerValue()).isActive = true
bottomLayoutConstraint(view: maskingView, containerView: containerView, viewController: presentationContext.viewControllerValue()).isActive = true
if let keyboardTrackingView = config.keyboardTrackingView {
maskingView.install(keyboardTrackingView: keyboardTrackingView)
}
// Update the container view's layout in order to know the masking view's frame
containerView.layoutIfNeeded()
}
func installInteractive() {
guard config.dimMode.modal else { return }
if config.dimMode.interactive {
maskingView.tappedHandler = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interactivelyHidden = true
strongSelf.delegate?.hide(presenter: strongSelf)
}
} else {
// There's no action to take, but the presence of
// a tap handler prevents interaction with underlying views.
maskingView.tappedHandler = { }
}
}
func installAccessibility() {
var elements: [NSObject] = []
if let accessibleMessage = view as? AccessibleMessage {
if let message = accessibleMessage.accessibilityMessage {
let element = accessibleMessage.accessibilityElement ?? view
element.isAccessibilityElement = true
if element.accessibilityLabel == nil {
element.accessibilityLabel = message
}
elements.append(element)
}
if let additional = accessibleMessage.additionalAccessibilityElements {
elements += additional
}
} else {
elements += [view]
}
if config.dimMode.interactive {
let dismissView = maskingViewOverlay()
dismissView.accessibilityLabel = config.dimModeAccessibilityLabel
dismissView.accessibilityTraits = UIAccessibilityTraits.button
elements.append(dismissView)
} else if config.dimMode.modal {
let plainView = maskingViewOverlay()
plainView.accessibilityTraits = UIAccessibilityTraits.none
elements.append(plainView)
}
if config.dimMode.modal {
maskingView.accessibilityViewIsModal = true
}
maskingView.accessibleElements = elements
}
func maskingViewOverlay() -> UIView {
let overlayView = UIView(frame: maskingView.bounds)
overlayView.translatesAutoresizingMaskIntoConstraints = true
overlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
maskingView.addSubview(overlayView)
maskingView.sendSubviewToBack(overlayView)
overlayView.isUserInteractionEnabled = false
overlayView.isAccessibilityElement = true
return overlayView
}
installed = true
guard let containerView = presentationContext.viewValue() else { return }
(presentationContext.viewControllerValue() as? WindowViewController)?.install()
installMaskingView(containerView: containerView)
installInteractive()
installAccessibility()
}
}
================================================
FILE: SwiftMessages/Resources/CardView.xib
================================================
================================================
FILE: SwiftMessages/Resources/CenteredView.xib
================================================
================================================
FILE: SwiftMessages/Resources/MessageView.xib
================================================
================================================
FILE: SwiftMessages/Resources/StatusLine.xib
================================================
================================================
FILE: SwiftMessages/Resources/TabView.xib
================================================
================================================
FILE: SwiftMessages/SwiftMessageModifier.swift
================================================
//
// SwiftMessageModifier.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
@available(iOS 14.0, *)
public extension View {
/// A view modifier for displaying a message using similar semantics to the `.sheet()` modifier.
func swiftMessage(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil,
@ViewBuilder messageContent: @escaping (Message) -> MessageContent
) -> some View where Message: Equatable & Identifiable, MessageContent: View {
swiftMessage(message: message, config: config, swiftMessages: swiftMessages) { message, _ in
messageContent(message)
}
}
/// A view modifier for displaying a message using similar semantics to the `.sheet()` modifier. This variant provides a
/// `SwiftMessageGeometryProxy`. The proxy is useful when one needs to know the geometry metrics of the container view,
/// particularly because `GeometryReader` doesn't work inside the view builder due to the way the message view is being
/// displayed from UIKit.
func swiftMessage(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil,
@ViewBuilder messageContent: @escaping (Message, MessageGeometryProxy) -> MessageContent
) -> some View where Message: Equatable & Identifiable, MessageContent: View {
modifier(
SwiftMessageModifier(
message: message,
config: config,
swiftMessages: swiftMessages,
messageContent: messageContent
)
)
}
/// A state-based modifier for displaying a message when `Message` conforms to `MessageViewConvertible`. This variant should be used if the message
/// view can be represented as pure data. If the message requires a delegate, has callbacks, etc., consider using the variant that takes a message view builder.
func swiftMessage(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil
) -> some View where Message: MessageViewConvertible {
swiftMessage(message: message, config: config, swiftMessages: swiftMessages) { content in
content.asMessageView()
}
}
}
@available(iOS 14.0, *)
private struct SwiftMessageModifier: ViewModifier where Message: Equatable & Identifiable, MessageContent: View {
// MARK: - API
fileprivate init(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil,
@ViewBuilder messageContent: @escaping (Message) -> MessageContent
) {
_message = message
self.config = config
self.swiftMessages = swiftMessages
self.messageContent = { message, _ in
messageContent(message)
}
}
fileprivate init(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil,
@ViewBuilder messageContent: @escaping (Message, MessageGeometryProxy) -> MessageContent
) {
_message = message
self.config = config
self.swiftMessages = swiftMessages
self.messageContent = messageContent
}
fileprivate init(
message: Binding,
config: SwiftMessages.Config? = nil,
swiftMessages: SwiftMessages? = nil
) where Message: MessageViewConvertible, Message.Content == MessageContent {
_message = message
self.config = config
self.swiftMessages = swiftMessages
self.messageContent = { message, _ in
message.asMessageView()
}
}
// MARK: - Constants
// MARK: - Variables
@Binding private var message: Message?
private let config: SwiftMessages.Config?
private let swiftMessages: SwiftMessages?
@ViewBuilder private let messageContent: (Message, MessageGeometryProxy) -> MessageContent
// MARK: - Body
func body(content: Content) -> some View {
content
.onChange(of: message) { message in
let show: @MainActor (SwiftMessages.Config, UIView) -> Void = swiftMessages?.show(config:view:) ?? SwiftMessages.show(config:view:)
let hideAll: @MainActor () -> Void = swiftMessages?.hideAll ?? SwiftMessages.hideAll
switch message {
case let message?:
let view = MessageHostingView(message: message, content: messageContent)
var config = config ?? swiftMessages?.defaultConfig ?? SwiftMessages.defaultConfig
config.eventListeners.append { event in
if case .didHide = event, event.id == self.message?.id {
self.message = nil
}
}
hideAll()
show(config, view)
case .none:
hideAll()
}
}
}
}
================================================
FILE: SwiftMessages/SwiftMessages.Config+Extensions.swift
================================================
//
// SwiftMessages.Config+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 12/26/20.
// Copyright © 2020 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension SwiftMessages.Config {
var windowLevel: UIWindow.Level? {
switch presentationContext {
case .window(let level): return level
case .windowScene(_, let level): return level
default: return nil
}
}
@available(iOS 13.0, *)
var windowScene: UIWindowScene? {
switch presentationContext {
case .windowScene(let scene, _): return scene as? UIWindowScene
default:
#if SWIFTMESSAGES_APP_EXTENSIONS
return nil
#else
return UIWindow.keyWindow?.windowScene
#endif
}
}
var shouldBecomeKeyWindow: Bool {
if let becomeKeyWindow = becomeKeyWindow { return becomeKeyWindow }
switch dimMode {
case .gray, .color, .blur:
// Should become key window in modal presentation style
// for proper VoiceOver handling.
return true
case .none:
return false
}
}
}
================================================
FILE: SwiftMessages/SwiftMessages.h
================================================
//
// SwiftMessages.h
// SwiftMessages
//
// Created by Timothy Moose on 8/9/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
#import
//! Project version number for SwiftMessages.
FOUNDATION_EXPORT double SwiftMessagesVersionNumber;
//! Project version string for SwiftMessages.
FOUNDATION_EXPORT const unsigned char SwiftMessagesVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import
================================================
FILE: SwiftMessages/SwiftMessages.swift
================================================
//
// SwiftMessages.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/1/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
private let globalInstance = SwiftMessages()
/**
The `SwiftMessages` class provides the interface for showing and hiding messages.
It behaves like a queue, only showing one message at a time. Message views that
adopt the `Identifiable` protocol (as `MessageView` does) will have duplicates removed.
*/
@MainActor
open class SwiftMessages {
/**
Specifies whether the message view is displayed at the top or bottom
of the selected presentation container.
*/
public enum PresentationStyle {
/**
Message view slides down from the top.
*/
case top
/**
Message view slides up from the bottom.
*/
case bottom
/**
Message view fades into the center.
*/
case center
/**
User-defined animation
*/
case custom(animator: Animator)
}
/**
Specifies how the container for presenting the message view
is selected.
*/
public enum PresentationContext {
/**
Displays the message view under navigation bars and tab bars if an
appropriate one is found. Otherwise, it is displayed in a new window
at level `UIWindow.Level.normal`. Use this option to automatically display
under bars, where applicable. Because this option involves a top-down
search, an appropriate context might not be found when the view controller
hierarchy incorporates custom containers. If this is the case, the
.ViewController option can provide a more targeted context.
*/
case automatic
/**
Displays the message in a new window at the specified window level.
SwiftMessages automatically increases the top margins of any message
view that adopts the `MarginInsetting` protocol (as `MessageView` does)
to account for the status bar. As of iOS 13, windows can no longer cover the
status bar. The only alternative is to set `Config.prefersStatusBarHidden = true`
to hide it.
*/
case window(windowLevel: UIWindow.Level)
/**
Displays the message in a new window, at the specified window level,
in the specified window scene. SwiftMessages automatically increases the top margins
of any message view that adopts the `MarginInsetting` protocol (as `MessageView` does)
to account for the status bar. As of iOS 13, windows can no longer cover the
status bar. The only alternative is to set `Config.prefersStatusBarHidden = true`
to hide it. The `WindowScene` protocol works around the change in Xcode 13 that prevents
using `@availability` attribute with `enum` cases containing associated values.
*/
case windowScene(_: WindowScene, windowLevel: UIWindow.Level)
/**
Displays the message view under navigation bars and tab bars if an
appropriate one is found using the given view controller as a starting
point and searching up the parent view controller chain. Otherwise, it
is displayed in the given view controller's view. This option can be used
for targeted placement in a view controller hierarchy.
*/
case viewController(_: UIViewController)
/**
Displays the message view in the given container view.
*/
case view(_: UIView)
}
/**
Specifies the duration of the message view's time on screen before it is
automatically hidden.
*/
public enum Duration {
/**
Hide the message view after the default duration.
*/
case automatic
/**
Disables automatic hiding of the message view.
*/
case forever
/**
Hide the message view after the speficied number of seconds.
- Parameter seconds: The number of seconds.
*/
case seconds(seconds: TimeInterval)
/**
The `indefinite` option is similar to `forever` in the sense that
the message view will not be automatically hidden. However, it
provides two options that can be useful in some scenarios:
- `delay`: wait the specified time interval before displaying
the message. If you hide the message during the delay
interval by calling either `hideAll()` or `hide(id:)`,
the message will not be displayed. This is not the case for
`hide()` because it only acts on a visible message. Messages
shown during another message's delay window are displayed first.
- `minimum`: if the message is displayed, ensure that it is displayed
for a minimum time interval. If you explicitly hide the
during this interval, the message will be hidden at the
end of the interval.
This option is useful for displaying a message when a process is taking
too long but you don't want to display the message if the process completes
in a reasonable amount of time. The value `indefinite(delay: 0, minimum: 0)`
is equivalent to `forever`.
For example, if a URL load is expected to complete in 2 seconds, you may use
the value `indefinite(delay: 2, minimum 1)` to ensure that the message will not
be displayed in most cases, but will be displayed for at least 1 second if
the operation takes longer than 2 seconds. By specifying a minimum duration,
you can avoid hiding the message too fast if the operation finishes right
after the delay interval.
*/
case indefinite(delay: TimeInterval, minimum: TimeInterval)
}
/**
Specifies notification's haptic feedback to be used on `MessageView` display
*/
/**
Specifies an optional haptic feedback to be used on `MessageView` display
*/
public enum Haptic {
case success
case warning
case error
}
/**
Specifies options for dimming the background behind the message view
similar to a popover view controller.
*/
public enum DimMode {
/**
Don't dim the background behind the message view.
*/
case none
/**
Dim the background behind the message view a gray color.
- `interactive`: Specifies whether or not tapping the
dimmed area dismisses the message view.
*/
case gray(interactive: Bool)
/**
Dim the background behind the message view using the given color.
SwiftMessages does not apply alpha transparency to the color, so any alpha
must be baked into the `UIColor` instance.
- `color`: The color of the dim view.
- `interactive`: Specifies whether or not tapping the
dimmed area dismisses the message view.
*/
case color(color: UIColor, interactive: Bool)
/**
Dim the background behind the message view using a blur effect with
the given style
- `style`: The blur effect style to use
- `alpha`: The alpha level of the blur
- `interactive`: Specifies whether or not tapping the
dimmed area dismisses the message view.
*/
case blur(style: UIBlurEffect.Style, alpha: CGFloat, interactive: Bool)
public var interactive: Bool {
switch self {
case .gray(let interactive):
return interactive
case .color(_, let interactive):
return interactive
case .blur (_, _, let interactive):
return interactive
case .none:
return false
}
}
public var modal: Bool {
switch self {
case .gray, .color, .blur:
return true
case .none:
return false
}
}
}
/**
Specifies events in the message lifecycle.
*/
public enum Event {
case willShow(UIView)
case didShow(UIView)
case willHide(UIView)
case didHide(UIView)
public var view: UIView {
switch self {
case .willShow(let view): return view
case .didShow(let view): return view
case .willHide(let view): return view
case .didHide(let view): return view
}
}
public var id: String? {
return (view as? Identifiable)?.id
}
}
/**
A closure that takes an `Event` as an argument.
*/
public typealias EventListener = (Event) -> Void
/**
The `Config` struct specifies options for displaying a single message view. It is
provided as an optional argument to one of the `MessageView.show()` methods.
*/
public struct Config {
public init() {}
/**
Specifies whether the message view is displayed at the top or bottom
of the selected presentation container. The default is `.Top`.
*/
public var presentationStyle = PresentationStyle.top
/**
Specifies how the container for presenting the message view
is selected. The default is `.Automatic`.
*/
public var presentationContext = PresentationContext.automatic
/**
Specifies the duration of the message view's time on screen before it is
automatically hidden. The default is `.Automatic`.
*/
public var duration = Duration.automatic
/**
Specifies options for dimming the background behind the message view
similar to a popover view controller. The default is `.None`.
*/
public var dimMode = DimMode.none
/**
Specifies notification's haptic feedback to be played on `MessageView` display.
No default value is provided.
*/
public var haptic: Haptic? = nil
/**
Specifies whether or not the interactive pan-to-hide gesture is enabled
on the message view. For views that implement the `BackgroundViewable`
protocol (as `MessageView` does), the pan gesture recognizer is installed
in the `backgroundView`, which allows for card-style views with transparent
margins that shouldn't be interactive. Otherwise, it is installed in
the message view itself. The default is `true`.
*/
public var interactiveHide = true
/**
Specifies the preferred status bar style when the view is being
displayed in a window. This can be useful when the view is being
displayed behind the status bar and the message view has a background
color that needs a different status bar style than the current one.
The default is `nil`.
*/
public var preferredStatusBarStyle: UIStatusBarStyle?
/**
Specifies the preferred status bar visibility when the view is being
displayed in a window. As of iOS 13, windows can no longer cover the
status bar. The only alternative is to hide the status bar by setting
this options to `true`. Default is `nil`.
*/
public var prefersStatusBarHidden: Bool?
/**
If a view controller is created to host the message view, should the view
controller auto rotate? The default is 'true', meaning it should auto
rotate.
*/
public var shouldAutorotate = true
/**
Specified whether or not duplicate `Identifiable` messages are ignored.
The default is `true`.
*/
public var ignoreDuplicates = true
/**
Specifies an optional array of event listeners.
*/
public var eventListeners: [EventListener] = []
/**
Specifies that in cases where the message is displayed in its own window,
such as with `.window` presentation context, the window should become
the key window. This option should only be used if the message view
needs to receive non-touch events, such as keyboard input. From Apple's
documentation https://developer.apple.com/reference/uikit/uiwindow:
> Whereas touch events are delivered to the window where they occurred,
> events that do not have a relevant coordinate value are delivered to
> the key window. Only one window at a time can be the key window, and
> you can use a window’s keyWindow property to determine its status.
> Most of the time, your app’s main window is the key window, but UIKit
> may designate a different window as needed.
*/
public var becomeKeyWindow: Bool?
/**
The `dimMode` background will use this accessibility
label, e.g. "dismiss" when the `interactive` option is used.
*/
public var dimModeAccessibilityLabel: String = "dismiss"
/**
The user interface style to use when SwiftMessages displays a message its own window.
Use with apps that don't support dark mode to prevent messages from adopting the
system's interface style.
*/
@available(iOS 13, *)
public var overrideUserInterfaceStyle: UIUserInterfaceStyle {
// Note that this is modelled as a computed property because
// Swift doesn't allow `@available` with stored properties.
get {
guard let rawValue = overrideUserInterfaceStyleRawValue else { return .unspecified }
return UIUserInterfaceStyle(rawValue: rawValue) ?? .unspecified
}
set {
overrideUserInterfaceStyleRawValue = newValue.rawValue
}
}
private var overrideUserInterfaceStyleRawValue: Int?
/**
If specified, SwiftMessages calls this closure when an instance of
`WindowViewController` is needed. Use this if you need to supply a custom subclass
of `WindowViewController`.
*/
public var windowViewController: ((_ config: SwiftMessages.Config) -> WindowViewController)?
/**
Supply an instance of `KeyboardTrackingView` to have the message view avoid the keyboard.
*/
public var keyboardTrackingView: KeyboardTrackingView?
/**
Specify a positive or negative priority to influence the position of a message in the queue based on it's relative priority.
*/
public var priority: Int = 0
}
/**
Not much to say here.
*/
nonisolated public init() {}
/**
Adds the given configuration and view to the message queue to be displayed.
- Parameter config: The configuration options.
- Parameter view: The view to be displayed.
*/
open func show(config: Config, view: UIView) {
let presenter = Presenter(config: config, view: view, delegate: self)
enqueue(presenter: presenter)
}
/**
Adds the given view to the message queue to be displayed
with default configuration options.
- Parameter config: The configuration options.
- Parameter view: The view to be displayed.
*/
public func show(view: UIView) {
show(config: defaultConfig, view: view)
}
/// A block that returns an arbitrary view.
public typealias ViewProvider = () -> UIView
/**
Adds the given configuration and view provider to the message queue to be displayed.
The `viewProvider` block is guaranteed to be called on the main queue where
it is safe to interact with `UIKit` components. This variant of `show()` is
recommended when the message might be added from a background queue.
- Parameter config: The configuration options.
- Parameter viewProvider: A block that returns the view to be displayed.
*/
nonisolated open func show(config: Config, viewProvider: @escaping ViewProvider) {
Task { @MainActor [weak self] in
guard let self else { return }
let view = viewProvider()
self.show(config: config, view: view)
}
}
/**
Adds the given view provider to the message queue to be displayed
with default configuration options.
The `viewProvider` block is guaranteed to be called on the main queue where
it is safe to interact with `UIKit` components. This variant of `show()` is
recommended when the message might be added from a background queue.
- Parameter viewProvider: A block that returns the view to be displayed.
*/
public func show(viewProvider: @escaping ViewProvider) {
show(config: defaultConfig, viewProvider: viewProvider)
}
/**
Hide the current message being displayed by animating it away.
*/
open func hide(animated: Bool = true) {
hideCurrent(animated: animated)
}
/**
Hide the current message, if there is one, by animating it away and
clear the message queue.
*/
open func hideAll() {
queue.removeAll()
delays.removeAll()
counts.removeAll()
hideCurrent()
}
/**
Hide a message with the given `id`. If the specified message is
currently being displayed, it will be animated away. Works with message
views, such as `MessageView`, that adopt the `Identifiable` protocol.
- Parameter id: The identifier of the message to remove.
*/
open func hide(id: String) {
if id == _current?.id {
hideCurrent()
}
queue = queue.filter { $0.id != id }
delays.remove(id: id)
counts[id] = nil
}
/**
Hide the message when the number of calls to show() and hideCounted(id:) for a
given message ID are equal. This can be useful for messages that may be
shown from multiple code paths to ensure that all paths are ready to hide.
*/
open func hideCounted(id: String) {
if let count = counts[id] {
if count < 2 {
counts[id] = nil
} else {
counts[id] = count - 1
return
}
}
if id == _current?.id {
hideCurrent()
}
queue = queue.filter { $0.id != id }
delays.remove(id: id)
}
/**
Get the count of a message with the given ID (see `hideCounted(id:)`)
*/
public func count(id: String) -> Int {
return counts[id] ?? 0
}
/**
Explicitly set the count of a message with the given ID (see `hideCounted(id:)`).
Not sure if there's a use case for this, but why not?!
*/
public func set(count: Int, for id: String) {
guard counts[id] != nil else { return }
return counts[id] = count
}
/**
Specifies the default configuration to use when calling the variants of
`show()` that don't take a `config` argument or as a base for custom configs.
*/
public var defaultConfig = Config()
/**
Specifies the amount of time to pause between removing a message
and showing the next. Default is 0.5 seconds.
*/
open var pauseBetweenMessages: TimeInterval = 0.5
/// Type for keeping track of delayed presentations
@MainActor
fileprivate class Delays {
fileprivate func add(presenter: Presenter) {
presenters.insert(presenter)
}
@discardableResult
fileprivate func remove(presenter: Presenter) -> Bool {
guard presenters.contains(presenter) else { return false }
presenters.remove(presenter)
return true
}
fileprivate func remove(id: String) {
presenters = presenters.filter { $0.id != id }
}
fileprivate func removeAll() {
presenters.removeAll()
}
private var presenters = Set()
}
func show(presenter: Presenter) {
enqueue(presenter: presenter)
}
fileprivate var queue: [Presenter] = []
fileprivate var delays = Delays()
fileprivate var counts: [String : Int] = [:]
fileprivate var _current: Presenter? = nil {
didSet {
if oldValue != nil {
Task { [weak self] in
try? await Task.sleep(seconds: self?.pauseBetweenMessages ?? 0)
self?.dequeueNext()
}
}
}
}
fileprivate func enqueue(presenter: Presenter) {
if presenter.config.ignoreDuplicates {
counts[presenter.id] = (counts[presenter.id] ?? 0) + 1
if let _current,
_current.id == presenter.id,
!_current.isHiding,
!_current.isOrphaned { return }
if queue.filter({ $0.id == presenter.id }).count > 0 { return }
}
func doEnqueue() {
queue.append(presenter)
dequeueNext()
}
if let delay = presenter.delayShow {
delays.add(presenter: presenter)
Task { [weak self] in
try? await Task.sleep(seconds: delay)
// Don't enqueue if the view has been hidden during the delay window.
guard let self, self.delays.remove(presenter: presenter) else { return }
doEnqueue()
}
} else {
doEnqueue()
}
}
fileprivate func dequeueNext() {
guard queue.count > 0 else { return }
if let _current, !_current.isOrphaned { return }
// Sort by priority
queue = queue.sorted { left, right in
left.config.priority > right.config.priority
}
let current = queue.removeFirst()
self._current = current
// Set `autohideToken` before the animation starts in case
// the dismiss gesture begins before we've queued the autohide
// block on animation completion.
self.autohideToken = current
current.showDate = CACurrentMediaTime()
do {
try current.show { [weak self] completed in
guard let self else { return }
guard completed else {
self.internalHide(presenter: current)
return
}
if current === self.autohideToken {
self.queueAutoHide()
}
}
} catch {
_current = nil
}
}
fileprivate func internalHide(presenter: Presenter) {
if presenter == _current {
hideCurrent()
} else {
queue = queue.filter { $0 != presenter }
delays.remove(presenter: presenter)
}
}
fileprivate func hideCurrent(animated: Bool = true) {
guard let current = _current, !current.isHiding else { return }
let action = { [weak self] in
current.hide(animated: animated) { (completed) in
guard completed, let self else { return }
guard self._current === current else { return }
self.counts[current.id] = nil
self._current = nil
}
}
let delay = current.delayHide ?? 0
Task {
try? await Task.sleep(seconds: delay)
action()
}
}
fileprivate weak var autohideToken: Presenter?
fileprivate func queueAutoHide() {
guard let current = _current else { return }
autohideToken = current
if let pauseDuration = current.pauseDuration {
Task { [weak self] in
try? await Task.sleep(seconds: pauseDuration)
// Make sure we've still got a green light to auto-hide.
guard let self, self.autohideToken == current else { return }
self.internalHide(presenter: current)
}
}
}
deinit {
guard let current = _current else { return }
Task { @MainActor [current] in
current.hide(animated: true) { _ in }
}
}
}
/*
MARK: - Accessing messages
*/
extension SwiftMessages {
/**
Returns the message view of type `T` if it is currently being shown or hidden.
- Returns: The view of type `T` if it is currently being shown or hidden.
*/
public func current() -> T? {
_current?.view as? T
}
/**
Returns a message view with the given `id` if it is currently being shown or hidden.
- Parameter id: The id of a message that adopts `Identifiable`.
- Returns: The view with matching id if currently being shown or hidden.
*/
public func current(id: String) -> T? {
_current?.id == id ? _current?.view as? T : nil
}
/**
Returns a message view with the given `id` if it is currently in the queue to be shown.
- Parameter id: The id of a message that adopts `Identifiable`.
- Returns: The view with matching id if currently queued to be shown.
*/
public func queued(id: String) -> T? {
queue.first { $0.id == id }?.view as? T
}
/**
Returns a message view with the given `id` if it is currently being
shown, hidden or in the queue to be shown.
- Parameter id: The id of a message that adopts `Identifiable`.
- Returns: The view with matching id if currently queued to be shown.
*/
public func currentOrQueued(id: String) -> T? {
return current(id: id) ?? queued(id: id)
}
}
/*
MARK: - PresenterDelegate
*/
extension SwiftMessages: PresenterDelegate {
func hide(presenter: Presenter) {
self.internalHide(presenter: presenter)
}
public func hide(animator: Animator) {
guard let presenter = self.presenter(forAnimator: animator) else { return }
self.internalHide(presenter: presenter)
}
public func panStarted(animator: Animator) {
autohideToken = nil
}
public func panEnded(animator: Animator) {
queueAutoHide()
}
private func presenter(forAnimator animator: Animator) -> Presenter? {
if let current = _current, animator === current.animator {
return current
}
let queued = queue.filter { $0.animator === animator }
return queued.first
}
}
/**
MARK: - Creating views from nibs
This extension provides several convenience functions for instantiating views from nib files.
SwiftMessages provides several default nib files in the Resources folder that can be
drag-and-dropped into a project as a starting point and modified.
*/
extension SwiftMessages {
/**
Loads a nib file with the same name as the generic view type `T` and returns
the first view found in the nib file with matching type `T`. For example, if
the generic type is `MyView`, a nib file named `MyView.nib` is loaded and the
first top-level view of type `MyView` is returned. The main bundle is searched
first followed by the SwiftMessages bundle.
- Parameter filesOwner: An optional files owner.
- Throws: `Error.CannotLoadViewFromNib` if a view matching the
generic type `T` is not found in the nib.
- Returns: An instance of generic view type `T`.
*/
public class func viewFromNib(_ filesOwner: AnyObject = NSNull.init()) throws -> T {
let name = T.description().components(separatedBy: ".").last
assert(name != nil)
let view: T = try internalViewFromNib(named: name!, bundle: nil, filesOwner: filesOwner)
return view
}
/**
Loads a nib file with specified name and returns the first view found in the nib file
with matching type `T`. The main bundle is searched first followed by the SwiftMessages bundle.
- Parameter name: The name of the nib file (excluding the .xib extension).
- Parameter filesOwner: An optional files owner.
- Throws: `Error.CannotLoadViewFromNib` if a view matching the
generic type `T` is not found in the nib.
- Returns: An instance of generic view type `T`.
*/
public class func viewFromNib(named name: String, filesOwner: AnyObject = NSNull.init()) throws -> T {
let view: T = try internalViewFromNib(named: name, bundle: nil, filesOwner: filesOwner)
return view
}
/**
Loads a nib file with specified name in the specified bundle and returns the
first view found in the nib file with matching type `T`.
- Parameter name: The name of the nib file (excluding the .xib extension).
- Parameter bundle: The name of the bundle containing the nib file.
- Parameter filesOwner: An optional files owner.
- Throws: `Error.CannotLoadViewFromNib` if a view matching the
generic type `T` is not found in the nib.
- Returns: An instance of generic view type `T`.
*/
public class func viewFromNib(named name: String, bundle: Bundle, filesOwner: AnyObject = NSNull.init()) throws -> T {
let view: T = try internalViewFromNib(named: name, bundle: bundle, filesOwner: filesOwner)
return view
}
fileprivate class func internalViewFromNib(named name: String, bundle: Bundle? = nil, filesOwner: AnyObject = NSNull.init()) throws -> T {
let resolvedBundle: Bundle
if let bundle = bundle {
resolvedBundle = bundle
} else {
if Bundle.main.path(forResource: name, ofType: "nib") != nil {
resolvedBundle = Bundle.main
} else {
resolvedBundle = Bundle.sm_frameworkBundle()
}
}
let arrayOfViews = resolvedBundle.loadNibNamed(name, owner: filesOwner, options: nil) ?? []
#if swift(>=4.1)
guard let view = arrayOfViews.compactMap( { $0 as? T} ).first else { throw SwiftMessagesError.cannotLoadViewFromNib(nibName: name) }
#else
guard let view = arrayOfViews.flatMap( { $0 as? T} ).first else { throw SwiftMessagesError.cannotLoadViewFromNib(nibName: name) }
#endif
return view
}
}
/*
MARK: - Static APIs
This extension provides a shared instance of `SwiftMessages` and a static API wrapper around
this instance for simplified syntax. For example, `SwiftMessages.show()` is equivalent
to `SwiftMessages.sharedInstance.show()`.
*/
extension SwiftMessages {
/**
A default shared instance of `SwiftMessages`. The `SwiftMessages` class provides
a set of static APIs that wrap calls to this instance. For example, `SwiftMessages.show()`
is equivalent to `SwiftMessages.sharedInstance.show()`.
*/
nonisolated public static var sharedInstance: SwiftMessages {
return globalInstance
}
public static func show(viewProvider: @escaping ViewProvider) {
globalInstance.show(viewProvider: viewProvider)
}
public static func show(config: Config, viewProvider: @escaping ViewProvider) {
globalInstance.show(config: config, viewProvider: viewProvider)
}
public static func show(view: UIView) {
globalInstance.show(view: view)
}
public static func show(config: Config, view: UIView) {
globalInstance.show(config: config, view: view)
}
public static func hide(animated: Bool = true) {
globalInstance.hide(animated: animated)
}
public static func hideAll() {
globalInstance.hideAll()
}
public static func hide(id: String) {
globalInstance.hide(id: id)
}
public static func hideCounted(id: String) {
globalInstance.hideCounted(id: id)
}
public static var defaultConfig: Config {
get {
return globalInstance.defaultConfig
}
set {
globalInstance.defaultConfig = newValue
}
}
public static var pauseBetweenMessages: TimeInterval {
get {
return globalInstance.pauseBetweenMessages
}
set {
globalInstance.pauseBetweenMessages = newValue
}
}
public static func current(id: String) -> T? {
return globalInstance.current(id: id)
}
public static func queued(id: String) -> T? {
return globalInstance.queued(id: id)
}
public static func currentOrQueued(id: String) -> T? {
return globalInstance.currentOrQueued(id: id)
}
public static func count(id: String) -> Int {
return globalInstance.count(id: id)
}
public static func set(count: Int, for id: String) {
globalInstance.set(count: count, for: id)
}
}
================================================
FILE: SwiftMessages/SwiftMessagesHideAction.swift
================================================
//
// SwiftMessagesHideKey.swift
// SwiftMessages
//
// Created by Mofe Ejegi on 11/05/2025.
// Copyright © 2025 SwiftKick Mobile. All rights reserved.
//
import SwiftUI
/// A SwiftUI-style action for dismissing the current SwiftMessage.
public struct SwiftMessagesHideAction {
public init() {}
/// Dismiss with option to disable animation.
@MainActor
public func callAsFunction(animated: Bool) {
SwiftMessages.hide(animated: animated)
}
}
public extension EnvironmentValues {
/// Inject `@Environment(\.swiftMessagesHide)` into your views to
/// access the SwiftUI-style action for dismissing the current SwiftMessage.
///
/// Usage:
/// ```swift
/// @Environment(\.swiftMessagesHide) private var hide
/// ```
///
/// Then you can call it like this:
/// ```swift
/// hide(animated: true)
/// ```
var swiftMessagesHide: SwiftMessagesHideAction {
get { self[SwiftMessagesHideKey.self] }
set { self[SwiftMessagesHideKey.self] = newValue }
}
}
private struct SwiftMessagesHideKey: EnvironmentKey {
/// Default to our action struct, which itself defaults to animated.
static let defaultValue: SwiftMessagesHideAction = SwiftMessagesHideAction()
}
================================================
FILE: SwiftMessages/SwiftMessagesSegue.swift
================================================
//
// SwiftMessagesSegue.swift
// SwiftMessages
//
// Created by Timothy Moose on 5/30/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import UIKit
/**
`SwiftMessagesSegue` is a configurable subclass of `UIStoryboardSegue` that utilizes
SwiftMessages to present and dismiss modal view controllers. It performs these transitions by
becoming your view controller's `transitioningDelegate` and calling SwiftMessage's `show()`
and `hide()` under the hood.
To use `SwiftMessagesSegue` with Interface Builder, control-drag a segue, then select
"swift messages" from the Segue Type dialog. This configures a default transition. There are
two suggested ways to further configure the transition by setting options on `SwiftMessagesSegue`.
First, and recommended, you may subclass `SwiftMessagesSegue` and override `init(identifier:source:destination:)`.
Subclasses will automatically appear in the segue type dialog using an auto-generated name (for example, the
name for "VeryNiceSegue" would be "very nice"). Second, you may override `prepare(for:sender:)` in the
presenting view controller and downcast the segue to `SwiftMessagesSegue`.
`SwiftMessagesSegue` can be used without an associated storyboard or segue by doing the following in
the presenting view controller.
let destinationVC = ... // make a reference to a destination view controller
let segue = SwiftMessagesSegue(identifier: nil, source: self, destination: destinationVC)
... // do any configuration here
segue.perform()
To dismiss, call the UIKit API on the presenting view controller:
dismiss(animated: true, completion: nil)
It is not necessary to retain `segue` because it retains itself until dismissal. However, you can
retain it if you plan to `perform()` more than once.
#### Present the controller on top of all controllers
If you don't know the presenter or you don't want to pass it as a source, like when you
have a completely separated message controller, you can pass a `WindowViewController`
as the `source` argument of the segue's initializer.
By default, the window will be shown in the current window scene at `.normal` window level.
However, these parameters can be customized by initializing the view controller with a `SwiftMessages.Config` that has the `SwiftMessages.Config.presentationContext` set to either `.window` or `.windowScene`:
+ note: Some additional details:
1. Your view controller's view will be embedded in a `SwiftMessages.BaseView` in order to
utilize some SwiftMessages features. This view can be accessed and configured via the
`SwiftMessagesSegue.messageView` property. For example, you may configure a default drop
shadow by calling `segue.messageView.configureDropShadow()`.
2. SwiftMessagesSegue provides static default view controller sizing based on device.
However, it is recommended that you specify sizing appropriate for your content using
one of the following methods.
1. Define sufficient width and height constraints in your view controller.
2. Set `preferredContentSize` (a.k.a "Use Preferred Explicit Size" in Interface Builder's
attribute inspector). Zeros are ignored, e.g. `CGSize(width: 0, height: 350)` only
affects the height.
3. Add explicit width and/or height constraints to `segue.messageView.backgroundView`.
Note that `Layout.topMessage` and `Layout.bottomMessage` are always full screen width.
For other layouts, the there is a maximum 500pt width on iPad (regular horizontal size class)
at 950 priority, which can be overridden by adding higher-priority constraints.
See the "View Controllers" selection in the Demo app for examples.
*/
open class SwiftMessagesSegue: UIStoryboardSegue {
/**
Specifies one of the pre-defined layouts, mirroring a subset of `MessageView.Layout`.
*/
public enum Layout {
/// The standard message view layout on top.
case topMessage
/// The standard message view layout on bottom.
case bottomMessage
/// A floating card-style view with rounded corners on top
case topCard
/// A floating tab-style view with rounded corners on bottom
case topTab
/// A floating card-style view with rounded corners on bottom
case bottomCard
/// A floating tab-style view with rounded corners on top
case bottomTab
/// A floating card-style view typically used with `.center` presentation style.
case centered
}
/**
Specifies how the view controller's view is installed into the
containing message view.
*/
public enum Containment {
/**
The view controller's view is installed for edge-to-edge display, extending into the safe areas
to the device edges. This is done by calling `messageView.installContentView(:insets:)`
See that method's documentation for additional details.
*/
case content
/**
The view controller's view is installed for card-style layouts, inset from the margins
and avoiding safe areas. This is done by calling `messageView.installBackgroundView(:insets:)`.
See that method's documentation for details.
*/
case background
/**
The view controller's view is installed for tab-style layouts, inset from the side margins, but extending
to the device edge on the top or bottom. This is done by calling `messageView.installBackgroundVerticalView(:insets:)`.
See that method's documentation for details.
*/
case backgroundVertical
}
/// The presentation style to use. See the SwiftMessages.PresentationStyle for details.
public var presentationStyle: SwiftMessages.PresentationStyle {
get { return messenger.defaultConfig.presentationStyle }
set { messenger.defaultConfig.presentationStyle = newValue }
}
/// The dim mode to use. See the SwiftMessages.DimMode for details.
public var dimMode: SwiftMessages.DimMode {
get { return messenger.defaultConfig.dimMode}
set { messenger.defaultConfig.dimMode = newValue }
}
// duration
public var duration: SwiftMessages.Duration {
get { return messenger.defaultConfig.duration}
set { messenger.defaultConfig.duration = newValue }
}
/// Specifies whether or not the interactive pan-to-hide gesture is enabled
/// on the message view. The default value is `true`, but may not be appropriate
/// for view controllers that use swipe or pan gestures.
public var interactiveHide: Bool {
get { return messenger.defaultConfig.interactiveHide }
set { messenger.defaultConfig.interactiveHide = newValue }
}
/// Specifies an optional array of event listeners.
public var eventListeners: [SwiftMessages.EventListener] {
get { return messenger.defaultConfig.eventListeners }
set { messenger.defaultConfig.eventListeners = newValue }
}
/**
Normally, the destination view controller's `modalPresentationStyle` is changed
to `.custom` in the `perform()` function. Set this property to `false` to prevent it from
being overridden.
*/
public var overrideModalPresentationStyle: Bool = true
/**
The view that is passed to `SwiftMessages.show(config:view:)` during presentation.
The view controller's view is installed into `containerView`, which is itself installed
into `messageView`. `SwiftMessagesSegue` does this installation automatically based on the
value of the `containment` property. `BaseView` is the parent of `MessageView` and provides a
number of configuration options that you may use. For example, you may configure a default drop
shadow by calling `messageView.configureDropShadow()`.
*/
public var messageView = BaseView()
/**
The view controller's view is embedded in `containerView` before being installed into
`messageView`. This view provides configurable squircle (round) corners (see the parent
class `CornerRoundingView`).
*/
public var containerView: CornerRoundingView = CornerRoundingView()
/**
Specifies how the view controller's view is installed into the
containing message view. See `Containment` for details.
*/
public var containment: Containment = .content
/**
Supply an instance of `KeyboardTrackingView` to have the message view avoid the keyboard.
*/
public var keyboardTrackingView: KeyboardTrackingView? {
get {
return messenger.defaultConfig.keyboardTrackingView
}
set {
messenger.defaultConfig.keyboardTrackingView = newValue
}
}
private var messenger = SwiftMessages()
private var selfRetainer: SwiftMessagesSegue? = nil
private lazy var hider = { return TransitioningDismisser(segue: self) }()
private lazy var presenter = {
return Presenter(config: messenger.defaultConfig, view: messageView, delegate: messenger)
}()
override open func perform() {
(source as? WindowViewController)?.install()
selfRetainer = self
startReleaseMonitor()
if overrideModalPresentationStyle {
destination.modalPresentationStyle = .custom
}
destination.transitioningDelegate = self
source.present(destination, animated: true, completion: nil)
}
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
dimMode = .gray(interactive: true)
messenger.defaultConfig.duration = .forever
}
fileprivate let safeAreaWorkaroundViewController = UIViewController()
/// The self-retainer will not allow the segue, presenting and presented view controllers to be released if the presenting view controller
/// is removed without first dismissing. This monitor handles that scenario by setting `self.selfRetainer = nil` if
/// the presenting view controller is no longer in the heirarchy.
private func startReleaseMonitor() {
Task { @MainActor [weak self] in
try? await Task.sleep(seconds: 2)
guard let self = self else { return }
switch self.source.view.window {
case .none: self.selfRetainer = nil
case .some: self.startReleaseMonitor()
}
}
}
}
extension SwiftMessagesSegue {
/// A convenience method for configuring some pre-defined layouts that mirror a subset of `MessageView.Layout`.
public func configure(layout: Layout) {
messageView.bounceAnimationOffset = 0
containment = .content
containerView.cornerRadius = 0
containerView.roundsLeadingCorners = false
messageView.configureDropShadow()
switch layout {
case .topMessage:
messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
messageView.collapseLayoutMarginAdditions = false
let animation = TopBottomAnimation(style: .top)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .bottomMessage:
messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
messageView.collapseLayoutMarginAdditions = false
let animation = TopBottomAnimation(style: .bottom)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .topCard:
containment = .background
messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .top
case .bottomCard:
containment = .background
messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .bottom
case .topTab:
containment = .backgroundVertical
messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
containerView.roundsLeadingCorners = true
let animation = TopBottomAnimation(style: .top)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .bottomTab:
containment = .backgroundVertical
messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
containerView.roundsLeadingCorners = true
let animation = TopBottomAnimation(style: .bottom)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .centered:
containment = .background
messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .center
}
}
}
extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let shower = TransitioningPresenter(segue: self)
let hider = self.hider
messenger.defaultConfig.eventListeners.append { [weak self] in
switch $0 {
case .didShow:
shower.completeTransition?(true)
case .didHide:
if let completeTransition = hider.completeTransition {
completeTransition(true)
} else {
// Case where message is internally hidden by SwiftMessages, such as with a
// dismiss gesture, rather than by view controller dismissal.
source.dismiss(animated: false, completion: nil)
}
(source as? WindowViewController)?.uninstall()
self?.selfRetainer = nil
default: break
}
}
return shower
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return hider
}
}
extension SwiftMessagesSegue {
private class TransitioningPresenter: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate private(set) var completeTransition: ((Bool) -> Void)?
private weak var segue: SwiftMessagesSegue?
fileprivate init(segue: SwiftMessagesSegue) {
self.segue = segue
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return segue?.presenter.animator.showDuration ?? 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let segue = segue,
let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
completeTransition = transitionContext.completeTransition
let transitionContainer = transitionContext.containerView
toView.translatesAutoresizingMaskIntoConstraints = false
segue.containerView.addSubview(toView)
segue.containerView.topAnchor.constraint(equalTo: toView.topAnchor).isActive = true
segue.containerView.bottomAnchor.constraint(equalTo: toView.bottomAnchor).isActive = true
segue.containerView.leadingAnchor.constraint(equalTo: toView.leadingAnchor).isActive = true
segue.containerView.trailingAnchor.constraint(equalTo: toView.trailingAnchor).isActive = true
// Install the `toView` into the message view.
switch segue.containment {
case .content:
segue.messageView.installContentView(segue.containerView)
case .background:
segue.messageView.installBackgroundView(segue.containerView)
case .backgroundVertical:
segue.messageView.installBackgroundVerticalView(segue.containerView)
}
let toVC = transitionContext.viewController(forKey: .to)
if let preferredHeight = toVC?.preferredContentSize.height,
preferredHeight > 0 {
segue.containerView.heightAnchor.constraint(equalToConstant: preferredHeight).with(priority: UILayoutPriority(rawValue: 951)).isActive = true
}
if let preferredWidth = toVC?.preferredContentSize.width,
preferredWidth > 0 {
segue.containerView.widthAnchor.constraint(equalToConstant: preferredWidth).with(priority: UILayoutPriority(rawValue: 951)).isActive = true
}
segue.presenter.config.presentationContext = .view(transitionContainer)
segue.messenger.show(presenter: segue.presenter)
}
}
}
extension SwiftMessagesSegue {
private class TransitioningDismisser: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate private(set) var completeTransition: ((Bool) -> Void)?
private weak var segue: SwiftMessagesSegue?
fileprivate init(segue: SwiftMessagesSegue) {
self.segue = segue
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return segue?.presenter.animator.hideDuration ?? 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let messenger = segue?.messenger else {
transitionContext.completeTransition(false)
return
}
completeTransition = transitionContext.completeTransition
messenger.hide()
}
}
}
================================================
FILE: SwiftMessages/Task+Extensions.swift
================================================
//
// Task+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 12/3/23.
// Copyright © 2023 SwiftKick Mobile. All rights reserved.
//
import Foundation
extension Task where Success == Never, Failure == Never {
static func sleep(seconds: TimeInterval) async throws {
try await sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}
}
================================================
FILE: SwiftMessages/Theme.swift
================================================
//
// Theme.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/7/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
/// The theme enum specifies the built-in theme options
public enum Theme {
case info
case success
case warning
case error
}
/// The Icon enum provides type-safe access to the included icons.
public enum Icon: String {
case error = "errorIcon"
case warning = "warningIcon"
case success = "successIcon"
case info = "infoIcon"
case errorLight = "errorIconLight"
case warningLight = "warningIconLight"
case successLight = "successIconLight"
case infoLight = "infoIconLight"
case errorSubtle = "errorIconSubtle"
case warningSubtle = "warningIconSubtle"
case successSubtle = "successIconSubtle"
case infoSubtle = "infoIconSubtle"
/// Returns the associated image.
public var image: UIImage {
return UIImage(named: rawValue, in: Bundle.sm_frameworkBundle(), compatibleWith: nil)!.withRenderingMode(.alwaysTemplate)
}
}
/// The IconStyle enum specifies the different variations of the included icons.
public enum IconStyle {
case `default`
case light
case subtle
case none
/// Returns the image for the given theme
public func image(theme: Theme) -> UIImage? {
switch (theme, self) {
case (.info, .default): return Icon.info.image
case (.info, .light): return Icon.infoLight.image
case (.info, .subtle): return Icon.infoSubtle.image
case (.success, .default): return Icon.success.image
case (.success, .light): return Icon.successLight.image
case (.success, .subtle): return Icon.successSubtle.image
case (.warning, .default): return Icon.warning.image
case (.warning, .light): return Icon.warningLight.image
case (.warning, .subtle): return Icon.warningSubtle.image
case (.error, .default): return Icon.error.image
case (.error, .light): return Icon.errorLight.image
case (.error, .subtle): return Icon.errorSubtle.image
default: return nil
}
}
}
================================================
FILE: SwiftMessages/TopBottomAnimation.swift
================================================
//
// TopBottomAnimation.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/4/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import UIKit
@MainActor
public class TopBottomAnimation: NSObject, Animator {
public weak var delegate: AnimationDelegate?
public let style: TopBottomAnimationStyle
public var showDuration: TimeInterval = 0.4
public var hideDuration: TimeInterval = 0.2
public var springDamping: CGFloat = 0.8
public var closeSpeedThreshold: CGFloat = 750.0;
public var closePercentThreshold: CGFloat = 0.33;
public var closeAbsoluteThreshold: CGFloat = 75.0;
public private(set) lazy var panGestureRecognizer: UIPanGestureRecognizer = {
let pan = UIPanGestureRecognizer()
pan.addTarget(self, action: #selector(pan(_:)))
return pan
}()
weak var messageView: UIView?
weak var containerView: UIView?
var context: AnimationContext?
public init(style: TopBottomAnimationStyle) {
self.style = style
}
init(style: TopBottomAnimationStyle, delegate: AnimationDelegate) {
self.style = style
self.delegate = delegate
}
public func show(context: AnimationContext, completion: @escaping AnimationCompletion) {
NotificationCenter.default.addObserver(self, selector: #selector(adjustMargins), name: UIDevice.orientationDidChangeNotification, object: nil)
install(context: context)
showAnimation(completion: completion)
}
public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) {
NotificationCenter.default.removeObserver(self)
let view = context.messageView
self.context = context
UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: {
switch self.style {
case .top:
view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height)
case .bottom:
view.transform = CGAffineTransform(translationX: 0, y: view.frame.maxY + view.frame.height)
}
}, completion: { completed in
#if SWIFTMESSAGES_APP_EXTENSIONS
completion(completed)
#else
// Fix #131 by always completing if application isn't active.
completion(completed || UIApplication.shared.applicationState != .active)
#endif
})
}
func install(context: AnimationContext) {
let view = context.messageView
let container = context.containerView
messageView = view
containerView = container
self.context = context
if let adjustable = context.messageView as? MarginAdjustable {
bounceOffset = adjustable.bounceAnimationOffset
}
view.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(view)
view.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
switch style {
case .top:
view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).with(priority: UILayoutPriority(200)).isActive = true
case .bottom:
view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset).with(priority: UILayoutPriority(200)).isActive = true
}
// Important to layout now in order to get the right safe area insets
container.layoutIfNeeded()
adjustMargins()
container.layoutIfNeeded()
let animationDistance = view.frame.height
switch style {
case .top:
view.transform = CGAffineTransform(translationX: 0, y: -animationDistance)
case .bottom:
view.transform = CGAffineTransform(translationX: 0, y: animationDistance)
}
if context.interactiveHide {
if let view = view as? BackgroundViewable {
view.backgroundView.addGestureRecognizer(panGestureRecognizer)
} else {
view.addGestureRecognizer(panGestureRecognizer)
}
}
if let view = view as? BackgroundViewable,
let cornerRoundingView = view.backgroundView as? CornerRoundingView,
cornerRoundingView.roundsLeadingCorners {
switch style {
case .top:
cornerRoundingView.roundedCorners = [.bottomLeft, .bottomRight]
case .bottom:
cornerRoundingView.roundedCorners = [.topLeft, .topRight]
}
}
}
@objc public func adjustMargins() {
guard let adjustable = messageView as? MarginAdjustable & UIView,
let context = context else { return }
adjustable.preservesSuperviewLayoutMargins = false
adjustable.insetsLayoutMarginsFromSafeArea = false
var layoutMargins = adjustable.defaultMarginAdjustment(context: context)
switch style {
case .top:
layoutMargins.top += bounceOffset
case .bottom:
layoutMargins.bottom += bounceOffset
}
adjustable.layoutMargins = layoutMargins
}
func showAnimation(completion: @escaping AnimationCompletion) {
guard let view = messageView else {
completion(false)
return
}
let animationDistance = abs(view.transform.ty)
// Cap the initial velocity at zero because the bounceOffset may not be great
// enough to allow for greater bounce induced by a quick panning motion.
let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, closeSpeed / animationDistance)
UIView.animate(withDuration: showDuration, delay: 0.0, usingSpringWithDamping: springDamping, initialSpringVelocity: initialSpringVelocity, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
view.transform = .identity
}, completion: { completed in
// Fix #131 by always completing if application isn't active.
#if SWIFTMESSAGES_APP_EXTENSIONS
completion(completed)
#else
completion(completed || UIApplication.shared.applicationState != .active)
#endif
})
}
fileprivate var bounceOffset: CGFloat = 5
/*
MARK: - Pan to close
*/
fileprivate var closing = false
fileprivate var rubberBanding = false
fileprivate var closeSpeed: CGFloat = 0.0
fileprivate var closePercent: CGFloat = 0.0
fileprivate var panTranslationY: CGFloat = 0.0
@objc func pan(_ pan: UIPanGestureRecognizer) {
switch pan.state {
case .changed:
guard let view = messageView else { return }
let height = view.bounds.height - bounceOffset
if height <= 0 { return }
var velocity = pan.velocity(in: view)
var translation = pan.translation(in: view)
if case .top = style {
velocity.y *= -1.0
translation.y *= -1.0
}
var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7)
if !closing {
// Turn on rubber banding if background view is inset from message view.
if let background = (messageView as? BackgroundViewable)?.backgroundView, background != view {
switch style {
case .top:
rubberBanding = background.frame.minY > 0
case .bottom:
rubberBanding = background.frame.maxY < view.bounds.height
}
}
if !rubberBanding && translationAmount < 0 { return }
closing = true
delegate?.panStarted(animator: self)
}
if !rubberBanding && translationAmount < 0 { translationAmount = 0 }
switch style {
case .top:
view.transform = CGAffineTransform(translationX: 0, y: -translationAmount)
case .bottom:
view.transform = CGAffineTransform(translationX: 0, y: translationAmount)
}
closeSpeed = velocity.y
closePercent = translation.y / height
panTranslationY = translation.y
case .ended, .cancelled:
if closeSpeed > closeSpeedThreshold || closePercent > closePercentThreshold || panTranslationY > closeAbsoluteThreshold {
delegate?.hide(animator: self)
} else {
closing = false
rubberBanding = false
closeSpeed = 0.0
closePercent = 0.0
panTranslationY = 0.0
showAnimation(completion: { (completed) in
self.delegate?.panEnded(animator: self)
})
}
default:
break
}
}
}
================================================
FILE: SwiftMessages/TopBottomAnimationStyle.swift
================================================
//
// TopBottomAnimationStyle.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/23/24.
// Copyright © 2024 SwiftKick Mobile. All rights reserved.
//
public enum TopBottomAnimationStyle {
case top
case bottom
}
================================================
FILE: SwiftMessages/TopBottomPresentable.swift
================================================
//
// File.swift
//
//
// Created by Julien Di Marco on 23/04/2024.
//
import Foundation
// MARK: - TopBottom Presentable Definition
@MainActor
protocol TopBottomPresentable {
var topBottomStyle: TopBottomAnimationStyle? { get }
}
// MARK: - TopBottom Presentable Conformances
extension TopBottomAnimation: TopBottomPresentable {
var topBottomStyle: TopBottomAnimationStyle? { return style }
}
extension PhysicsAnimation: TopBottomPresentable {
var topBottomStyle: TopBottomAnimationStyle? {
switch placement {
case .top: return .top
case .bottom: return .bottom
default: return nil
}
}
}
// MARK: - Presentation Style Convenience
extension SwiftMessages.PresentationStyle {
/// A temporary workaround to allow custom presentation contexts using `TopBottomAnimation`
/// to display properly behind bars. THe long term solution is to refactor all of the
/// presentation context logic to work with safe area insets.
@MainActor
var topBottomStyle: TopBottomAnimationStyle? {
switch self {
case .top: return .top
case .bottom: return .bottom
case .custom(let animator as TopBottomPresentable): return animator.topBottomStyle
case .center: return nil
default: return nil
}
}
}
================================================
FILE: SwiftMessages/UIEdgeInsets+Extensions.swift
================================================
//
// UIEdgeInsets+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 5/23/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension UIEdgeInsets {
public static func +(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets {
let topSum = left.top + right.top
let leftSum = left.left + right.left
let bottomSum = left.bottom + right.bottom
let rightSum = left.right + right.right
return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum)
}
public static func -(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets {
let topSum = left.top - right.top
let leftSum = left.left - right.left
let bottomSum = left.bottom - right.bottom
let rightSum = left.right - right.right
return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum)
}
}
================================================
FILE: SwiftMessages/UIViewController+Extensions.swift
================================================
//
// UIViewController+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/5/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
extension UIViewController {
func sm_selectPresentationContextTopDown(_ config: SwiftMessages.Config) -> UIViewController {
let topBottomStyle = config.presentationStyle.topBottomStyle
if let presented = presentedViewController {
return presented.sm_selectPresentationContextTopDown(config)
} else if case .top? = topBottomStyle, let navigationController = sm_selectNavigationControllerTopDown() {
return navigationController
} else if case .bottom? = topBottomStyle, let tabBarController = sm_selectTabBarControllerTopDown() {
return tabBarController
}
return WindowViewController.newInstance(config: config)
}
fileprivate func sm_selectNavigationControllerTopDown() -> UINavigationController? {
if let presented = presentedViewController {
return presented.sm_selectNavigationControllerTopDown()
} else if let navigationController = self as? UINavigationController {
if navigationController.sm_isVisible(view: navigationController.navigationBar) {
return navigationController
}
return navigationController.topViewController?.sm_selectNavigationControllerTopDown()
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.sm_selectNavigationControllerTopDown()
}
return nil
}
fileprivate func sm_selectTabBarControllerTopDown() -> UITabBarController? {
if let presented = presentedViewController {
return presented.sm_selectTabBarControllerTopDown()
} else if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.sm_selectTabBarControllerTopDown()
} else if let tabBarController = self as? UITabBarController {
if tabBarController.sm_isVisible(view: tabBarController.tabBar) {
return tabBarController
}
return tabBarController.selectedViewController?.sm_selectTabBarControllerTopDown()
}
return nil
}
func sm_selectPresentationContextBottomUp(_ config: SwiftMessages.Config) -> UIViewController {
let topBottomStyle = config.presentationStyle.topBottomStyle
if let parent = parent {
if let navigationController = parent as? UINavigationController {
if case .top? = topBottomStyle, navigationController.sm_isVisible(view: navigationController.navigationBar) {
return navigationController
}
return navigationController.sm_selectPresentationContextBottomUp(config)
} else if let tabBarController = parent as? UITabBarController {
if case .bottom? = topBottomStyle, tabBarController.sm_isVisible(view: tabBarController.tabBar) {
return tabBarController
}
return tabBarController.sm_selectPresentationContextBottomUp(config)
}
}
if self.view is UITableView {
// Never select scroll view as presentation context
// because, you know, it scrolls.
if let parent = self.parent {
return parent.sm_selectPresentationContextBottomUp(config)
} else {
return WindowViewController.newInstance(config: config)
}
}
return self
}
func sm_isVisible(view: UIView) -> Bool {
if view.isHidden { return false }
if view.alpha == 0.0 { return false }
let frame = self.view.convert(view.bounds, from: view)
if !self.view.bounds.intersects(frame) { return false }
return true
}
}
================================================
FILE: SwiftMessages/UIWindow+Extensions.swift
================================================
//
// UIWindow+Extensions.swift
// SwiftMessages
//
// Created by Timothy Moose on 3/11/21.
// Copyright © 2021 SwiftKick Mobile. All rights reserved.
//
import UIKit
extension UIWindow {
#if !SWIFTMESSAGES_APP_EXTENSIONS
static var keyWindow: UIWindow? {
return UIApplication.shared.connectedScenes
.sorted { $0.activationState.sortPriority < $1.activationState.sortPriority }
.compactMap { $0 as? UIWindowScene }
.compactMap { $0.windows.first { $0.isKeyWindow } }
.first
}
#endif
}
@available(iOS 13.0, *)
private extension UIScene.ActivationState {
var sortPriority: Int {
switch self {
case .foregroundActive: return 1
case .foregroundInactive: return 2
case .background: return 3
case .unattached: return 4
@unknown default: return 5
}
}
}
================================================
FILE: SwiftMessages/Weak.swift
================================================
//
// Weak.swift
// SwiftMessages
//
// Created by Timothy Moose on 6/4/17.
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
//
import Foundation
public class Weak {
public weak var value : T?
public init(value: T?) {
self.value = value
}
}
================================================
FILE: SwiftMessages/WindowScene.swift
================================================
import Foundation
import UIKit
/// A workaround for the change in Xcode 13 that prevents using `@availability` attribute
/// with `enum` cases containing associated values.
public protocol WindowScene {}
@available(iOS 13.0, *)
extension UIWindowScene: WindowScene {}
================================================
FILE: SwiftMessages/WindowViewController.swift
================================================
//
// WindowViewController.swift
// SwiftMessages
//
// Created by Timothy Moose on 8/1/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import UIKit
open class WindowViewController: UIViewController
{
override open var shouldAutorotate: Bool {
return config.shouldAutorotate
}
convenience public init() {
self.init(config: SwiftMessages.Config())
}
public init(config: SwiftMessages.Config) {
self.config = config
let view = PassthroughView()
let window = PassthroughWindow(hitTestView: view)
self.window = window
super.init(nibName: nil, bundle: nil)
self.view = view
window.rootViewController = self
window.windowLevel = config.windowLevel ?? UIWindow.Level.normal
window.overrideUserInterfaceStyle = config.overrideUserInterfaceStyle
}
func install() {
window?.windowScene = config.windowScene
#if !SWIFTMESSAGES_APP_EXTENSIONS
previousKeyWindow = UIWindow.keyWindow
#endif
show(
becomeKey: config.shouldBecomeKeyWindow,
frame: config.windowScene?.coordinateSpace.bounds
)
}
private func show(becomeKey: Bool, frame: CGRect? = nil) {
guard let window = window else { return }
window.frame = frame ?? UIScreen.main.bounds
if becomeKey {
window.makeKeyAndVisible()
} else {
window.isHidden = false
}
}
func uninstall() {
if window?.isKeyWindow == true {
previousKeyWindow?.makeKey()
}
window?.windowScene = nil
window?.isHidden = true
window = nil
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open var preferredStatusBarStyle: UIStatusBarStyle {
return config.preferredStatusBarStyle ?? super.preferredStatusBarStyle
}
open override var prefersStatusBarHidden: Bool {
return config.prefersStatusBarHidden ?? super.prefersStatusBarHidden
}
// MARK: - Variables
private var window: UIWindow?
private weak var previousKeyWindow: UIWindow?
let config: SwiftMessages.Config
}
extension WindowViewController {
static func newInstance(config: SwiftMessages.Config) -> WindowViewController {
return config.windowViewController?(config) ?? WindowViewController(config: config)
}
}
================================================
FILE: SwiftMessages.podspec
================================================
Pod::Spec.new do |spec|
spec.name = 'SwiftMessages'
spec.version = '10.0.2'
spec.license = { :type => 'MIT' }
spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages'
spec.authors = { 'Timothy Moose' => 'tim@swiftkickmobile.com' }
spec.summary = 'A very flexible message bar for iOS written in Swift.'
spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => spec.version}
spec.platform = :ios, '13.0'
spec.swift_version = '5.0'
spec.ios.deployment_target = '13.0'
spec.framework = 'UIKit'
spec.requires_arc = true
spec.default_subspec = 'App'
spec.subspec 'App' do |app|
app.source_files = 'SwiftMessages/**/*.swift'
app.resource_bundles = {'SwiftMessages' => ['SwiftMessages/Resources/*.*']}
end
spec.subspec 'AppExtension' do |ext|
ext.source_files = 'SwiftMessages/**/*.swift'
ext.exclude_files = 'SwiftMessages/**/SegueConvenienceClasses.swift'
ext.resource_bundles = {'SwiftMessages_SwiftMessages' => ['SwiftMessages/Resources/**/*.*']}
# For app extensions, disabling code paths using unavailable API
ext.pod_target_xcconfig = {
'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'SWIFTMESSAGES_APP_EXTENSIONS',
'GCC_PREPROCESSOR_DEFINITIONS' => 'SWIFTMESSAGES_APP_EXTENSIONS=1'
}
end
end
================================================
FILE: SwiftMessages.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
0797E40E26EE12B400691606 /* WindowScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0797E40D26EE12B400691606 /* WindowScene.swift */; };
220655121FAF82B600F4E00F /* MarginAdjustable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220655111FAF82B600F4E00F /* MarginAdjustable+Extensions.swift */; };
220D386E2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D386D2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift */; };
223DE69D2C29E50C000161E5 /* MessageGeometryProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223DE69C2C29E50B000161E5 /* MessageGeometryProxy.swift */; };
224C3C902C28A2F900B50B18 /* TopBottomPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224C3C8F2C28A2F900B50B18 /* TopBottomPresentable.swift */; };
224C3C932C28BC4900B50B18 /* TopBottomAnimationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224C3C922C28BC4400B50B18 /* TopBottomAnimationStyle.swift */; };
224FB69921153B440081D4DE /* CALayer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224FB69821153B440081D4DE /* CALayer+Extensions.swift */; };
225304622290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225304612290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift */; };
225304662293000C00A03ACF /* KeyboardTrackingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225304652293000C00A03ACF /* KeyboardTrackingView.swift */; };
2270044B1FAFA6DD0045DDC3 /* PhysicsAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2270044A1FAFA6DD0045DDC3 /* PhysicsAnimation.swift */; };
22774BA020B5EF2A00813732 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22774B9F20B5EF2A00813732 /* UIEdgeInsets+Extensions.swift */; };
227BA6D920BF224A00E5A843 /* SwiftMessagesSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227BA6D820BF224A00E5A843 /* SwiftMessagesSegue.swift */; };
228DF5261FACAC51004F8A39 /* errorIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5231FACAC51004F8A39 /* errorIcon.png */; };
228DF5271FACAC51004F8A39 /* errorIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5241FACAC51004F8A39 /* errorIcon@2x.png */; };
228DF5281FACAC51004F8A39 /* errorIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5251FACAC51004F8A39 /* errorIcon@3x.png */; };
228DF54A1FAD0806004F8A39 /* successIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5291FAD0802004F8A39 /* successIconLight@3x.png */; };
228DF54B1FAD0806004F8A39 /* warningIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52A1FAD0802004F8A39 /* warningIconLight.png */; };
228DF54C1FAD0806004F8A39 /* errorIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52B1FAD0802004F8A39 /* errorIconLight.png */; };
228DF54D1FAD0806004F8A39 /* infoIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52C1FAD0803004F8A39 /* infoIcon@2x.png */; };
228DF54E1FAD0806004F8A39 /* warningIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52D1FAD0803004F8A39 /* warningIconSubtle.png */; };
228DF54F1FAD0806004F8A39 /* successIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52E1FAD0803004F8A39 /* successIconSubtle@3x.png */; };
228DF5501FAD0806004F8A39 /* infoIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF52F1FAD0803004F8A39 /* infoIcon.png */; };
228DF5511FAD0806004F8A39 /* infoIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5301FAD0803004F8A39 /* infoIconLight@2x.png */; };
228DF5521FAD0806004F8A39 /* successIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5311FAD0803004F8A39 /* successIcon.png */; };
228DF5531FAD0806004F8A39 /* successIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5321FAD0803004F8A39 /* successIcon@2x.png */; };
228DF5541FAD0806004F8A39 /* successIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5331FAD0803004F8A39 /* successIcon@3x.png */; };
228DF5551FAD0806004F8A39 /* warningIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5341FAD0803004F8A39 /* warningIcon@2x.png */; };
228DF5561FAD0806004F8A39 /* successIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5351FAD0803004F8A39 /* successIconSubtle.png */; };
228DF5571FAD0806004F8A39 /* warningIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5361FAD0803004F8A39 /* warningIconSubtle@2x.png */; };
228DF5581FAD0806004F8A39 /* errorIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5371FAD0803004F8A39 /* errorIconSubtle.png */; };
228DF5591FAD0806004F8A39 /* successIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5381FAD0803004F8A39 /* successIconSubtle@2x.png */; };
228DF55A1FAD0806004F8A39 /* errorIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5391FAD0804004F8A39 /* errorIconLight@3x.png */; };
228DF55B1FAD0806004F8A39 /* warningIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53A1FAD0804004F8A39 /* warningIconLight@3x.png */; };
228DF55C1FAD0806004F8A39 /* warningIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53B1FAD0804004F8A39 /* warningIcon.png */; };
228DF55D1FAD0806004F8A39 /* warningIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53C1FAD0804004F8A39 /* warningIconLight@2x.png */; };
228DF55E1FAD0806004F8A39 /* warningIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53D1FAD0804004F8A39 /* warningIconSubtle@3x.png */; };
228DF55F1FAD0806004F8A39 /* errorIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53E1FAD0804004F8A39 /* errorIconLight@2x.png */; };
228DF5601FAD0806004F8A39 /* warningIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF53F1FAD0804004F8A39 /* warningIcon@3x.png */; };
228DF5611FAD0806004F8A39 /* infoIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5401FAD0804004F8A39 /* infoIcon@3x.png */; };
228DF5621FAD0806004F8A39 /* infoIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5411FAD0804004F8A39 /* infoIconLight.png */; };
228DF5631FAD0806004F8A39 /* infoIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5421FAD0804004F8A39 /* infoIconSubtle@2x.png */; };
228DF5641FAD0806004F8A39 /* errorIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5431FAD0805004F8A39 /* errorIconSubtle@2x.png */; };
228DF5651FAD0806004F8A39 /* successIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5441FAD0805004F8A39 /* successIconLight@2x.png */; };
228DF5661FAD0806004F8A39 /* errorIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5451FAD0805004F8A39 /* errorIconSubtle@3x.png */; };
228DF5671FAD0806004F8A39 /* infoIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5461FAD0805004F8A39 /* infoIconLight@3x.png */; };
228DF5681FAD0806004F8A39 /* infoIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5471FAD0805004F8A39 /* infoIconSubtle.png */; };
228DF5691FAD0806004F8A39 /* successIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5481FAD0805004F8A39 /* successIconLight.png */; };
228DF56A1FAD0806004F8A39 /* infoIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */; };
228F7DDE2ACF703A006C9644 /* MessageHostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */; };
228F7DDF2ACF703A006C9644 /* SwiftMessageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */; };
228F7DE02ACF703A006C9644 /* MessageViewConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */; };
22982C172B6030B000852311 /* HapticMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22982C162B6030B000852311 /* HapticMessage.swift */; };
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2041EE47DC900E2DDC1 /* Weak.swift */; };
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2061EE480D000E2DDC1 /* Animator.swift */; };
2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */; };
229F778125FAB1E9008C2ACB /* UIWindow+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */; };
22D3B4562B1CEF76002D8665 /* Task+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */; };
22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */; };
22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */; };
22E01F641E74EC8B00ACE19A /* MaskingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E01F631E74EC8B00ACE19A /* MaskingView.swift */; };
22E307FF1E74C5B100E35893 /* AccessibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */; };
22F27951210CE25900273E7F /* CornerRoundingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F27950210CE25900273E7F /* CornerRoundingView.swift */; };
86589D471D64B6E40041676C /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86589D461D64B6E40041676C /* BaseView.swift */; };
86589D911D692B1C0041676C /* TabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86589D901D692B1B0041676C /* TabView.xib */; };
867BED211D622793005212E3 /* BackgroundViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867BED201D622793005212E3 /* BackgroundViewable.swift */; };
86B48AEF1D5A41C900063E2B /* SwiftMessages.h in Headers */ = {isa = PBXBuildFile; fileRef = 86B48AEE1D5A41C900063E2B /* SwiftMessages.h */; settings = {ATTRIBUTES = (Public, ); }; };
86B48AF61D5A41C900063E2B /* SwiftMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86B48AEC1D5A41C900063E2B /* SwiftMessages.framework */; };
86B48AFB1D5A41C900063E2B /* SwiftMessagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B48AFA1D5A41C900063E2B /* SwiftMessagesTests.swift */; };
86BBA8F91D5E01FC00FE8F16 /* CardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86BBA8F81D5E01FC00FE8F16 /* CardView.xib */; };
86BBA8FC1D5E03F100FE8F16 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867E21821D4D025200594A41 /* MessageView.swift */; };
86BBA8FD1D5E03F800FE8F16 /* SwiftMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 864495581D4FA0AD0056EB2A /* SwiftMessages.swift */; };
86BBA8FF1D5E040600FE8F16 /* Presenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867E21931D4D50BB00594A41 /* Presenter.swift */; };
86BBA9001D5E040600FE8F16 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF8171D54F0650031EE32 /* PassthroughView.swift */; };
86BBA9011D5E040600FE8F16 /* PassthroughWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF8191D54F0850031EE32 /* PassthroughWindow.swift */; };
86BBA9021D5E040600FE8F16 /* WindowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8644955C1D4FAF7C0056EB2A /* WindowViewController.swift */; };
86BBA9031D5E040600FE8F16 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF81B1D551FE60031EE32 /* UIViewController+Extensions.swift */; };
86BBA9041D5E040600FE8F16 /* NSBundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 862C0CB01D5911C100D06168 /* NSBundle+Extensions.swift */; };
86BBA9051D5E040C00FE8F16 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF82C1D580F410031EE32 /* Theme.swift */; };
86BBA9061D5E040C00FE8F16 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 864495551D4F7C390056EB2A /* Identifiable.swift */; };
86BBA9071D5E040C00FE8F16 /* MarginAdjustable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF81D1D5549680031EE32 /* MarginAdjustable.swift */; };
86BBA9081D5E040C00FE8F16 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86AAF82A1D580DD70031EE32 /* Error.swift */; };
B0E55A662DD110EA003D97B1 /* SwiftMessagesHideAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0E55A652DD110DB003D97B1 /* SwiftMessagesHideAction.swift */; };
E6E49F911D70A344006CB883 /* MessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 862C0CDA1D5A397F00D06168 /* MessageView.xib */; };
E6E49F921D70A349006CB883 /* StatusLine.xib in Resources */ = {isa = PBXBuildFile; fileRef = 862C0CDB1D5A397F00D06168 /* StatusLine.xib */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
86B48AF71D5A41C900063E2B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 867E21471D4D01D500594A41 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 86B48AEB1D5A41C900063E2B;
remoteInfo = SwiftMessages;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
0797E40D26EE12B400691606 /* WindowScene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowScene.swift; sourceTree = ""; };
220655111FAF82B600F4E00F /* MarginAdjustable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MarginAdjustable+Extensions.swift"; sourceTree = ""; };
220D386D2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftMessages.Config+Extensions.swift"; sourceTree = ""; };
223DE69C2C29E50B000161E5 /* MessageGeometryProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGeometryProxy.swift; sourceTree = ""; };
224C3C8F2C28A2F900B50B18 /* TopBottomPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBottomPresentable.swift; sourceTree = ""; };
224C3C922C28BC4400B50B18 /* TopBottomAnimationStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBottomAnimationStyle.swift; sourceTree = ""; };
224FB69821153B440081D4DE /* CALayer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Extensions.swift"; sourceTree = ""; };
225304612290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extensions.swift"; sourceTree = ""; };
225304652293000C00A03ACF /* KeyboardTrackingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardTrackingView.swift; sourceTree = ""; };
2270044A1FAFA6DD0045DDC3 /* PhysicsAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhysicsAnimation.swift; sourceTree = ""; };
22774B9F20B5EF2A00813732 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; };
227BA6D820BF224A00E5A843 /* SwiftMessagesSegue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMessagesSegue.swift; sourceTree = ""; };
228DF5231FACAC51004F8A39 /* errorIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = errorIcon.png; path = Resources/errorIcon.png; sourceTree = ""; };
228DF5241FACAC51004F8A39 /* errorIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIcon@2x.png"; path = "Resources/errorIcon@2x.png"; sourceTree = ""; };
228DF5251FACAC51004F8A39 /* errorIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIcon@3x.png"; path = "Resources/errorIcon@3x.png"; sourceTree = ""; };
228DF5291FAD0802004F8A39 /* successIconLight@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIconLight@3x.png"; path = "Resources/successIconLight@3x.png"; sourceTree = ""; };
228DF52A1FAD0802004F8A39 /* warningIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = warningIconLight.png; path = Resources/warningIconLight.png; sourceTree = ""; };
228DF52B1FAD0802004F8A39 /* errorIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = errorIconLight.png; path = Resources/errorIconLight.png; sourceTree = ""; };
228DF52C1FAD0803004F8A39 /* infoIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIcon@2x.png"; path = "Resources/infoIcon@2x.png"; sourceTree = ""; };
228DF52D1FAD0803004F8A39 /* warningIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = warningIconSubtle.png; path = Resources/warningIconSubtle.png; sourceTree = ""; };
228DF52E1FAD0803004F8A39 /* successIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIconSubtle@3x.png"; path = "Resources/successIconSubtle@3x.png"; sourceTree = ""; };
228DF52F1FAD0803004F8A39 /* infoIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = infoIcon.png; path = Resources/infoIcon.png; sourceTree = ""; };
228DF5301FAD0803004F8A39 /* infoIconLight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconLight@2x.png"; path = "Resources/infoIconLight@2x.png"; sourceTree = ""; };
228DF5311FAD0803004F8A39 /* successIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = successIcon.png; path = Resources/successIcon.png; sourceTree = ""; };
228DF5321FAD0803004F8A39 /* successIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIcon@2x.png"; path = "Resources/successIcon@2x.png"; sourceTree = ""; };
228DF5331FAD0803004F8A39 /* successIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIcon@3x.png"; path = "Resources/successIcon@3x.png"; sourceTree = ""; };
228DF5341FAD0803004F8A39 /* warningIcon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIcon@2x.png"; path = "Resources/warningIcon@2x.png"; sourceTree = ""; };
228DF5351FAD0803004F8A39 /* successIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = successIconSubtle.png; path = Resources/successIconSubtle.png; sourceTree = ""; };
228DF5361FAD0803004F8A39 /* warningIconSubtle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIconSubtle@2x.png"; path = "Resources/warningIconSubtle@2x.png"; sourceTree = ""; };
228DF5371FAD0803004F8A39 /* errorIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = errorIconSubtle.png; path = Resources/errorIconSubtle.png; sourceTree = ""; };
228DF5381FAD0803004F8A39 /* successIconSubtle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIconSubtle@2x.png"; path = "Resources/successIconSubtle@2x.png"; sourceTree = ""; };
228DF5391FAD0804004F8A39 /* errorIconLight@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIconLight@3x.png"; path = "Resources/errorIconLight@3x.png"; sourceTree = ""; };
228DF53A1FAD0804004F8A39 /* warningIconLight@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIconLight@3x.png"; path = "Resources/warningIconLight@3x.png"; sourceTree = ""; };
228DF53B1FAD0804004F8A39 /* warningIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = warningIcon.png; path = Resources/warningIcon.png; sourceTree = ""; };
228DF53C1FAD0804004F8A39 /* warningIconLight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIconLight@2x.png"; path = "Resources/warningIconLight@2x.png"; sourceTree = ""; };
228DF53D1FAD0804004F8A39 /* warningIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIconSubtle@3x.png"; path = "Resources/warningIconSubtle@3x.png"; sourceTree = ""; };
228DF53E1FAD0804004F8A39 /* errorIconLight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIconLight@2x.png"; path = "Resources/errorIconLight@2x.png"; sourceTree = ""; };
228DF53F1FAD0804004F8A39 /* warningIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "warningIcon@3x.png"; path = "Resources/warningIcon@3x.png"; sourceTree = ""; };
228DF5401FAD0804004F8A39 /* infoIcon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIcon@3x.png"; path = "Resources/infoIcon@3x.png"; sourceTree = ""; };
228DF5411FAD0804004F8A39 /* infoIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = infoIconLight.png; path = Resources/infoIconLight.png; sourceTree = ""; };
228DF5421FAD0804004F8A39 /* infoIconSubtle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconSubtle@2x.png"; path = "Resources/infoIconSubtle@2x.png"; sourceTree = ""; };
228DF5431FAD0805004F8A39 /* errorIconSubtle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIconSubtle@2x.png"; path = "Resources/errorIconSubtle@2x.png"; sourceTree = ""; };
228DF5441FAD0805004F8A39 /* successIconLight@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "successIconLight@2x.png"; path = "Resources/successIconLight@2x.png"; sourceTree = ""; };
228DF5451FAD0805004F8A39 /* errorIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "errorIconSubtle@3x.png"; path = "Resources/errorIconSubtle@3x.png"; sourceTree = ""; };
228DF5461FAD0805004F8A39 /* infoIconLight@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconLight@3x.png"; path = "Resources/infoIconLight@3x.png"; sourceTree = ""; };
228DF5471FAD0805004F8A39 /* infoIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = infoIconSubtle.png; path = Resources/infoIconSubtle.png; sourceTree = ""; };
228DF5481FAD0805004F8A39 /* successIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = successIconLight.png; path = Resources/successIconLight.png; sourceTree = ""; };
228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconSubtle@3x.png"; path = "Resources/infoIconSubtle@3x.png"; sourceTree = ""; };
228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHostingView.swift; sourceTree = ""; };
228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftMessageModifier.swift; sourceTree = ""; };
228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewConvertible.swift; sourceTree = ""; };
22982C162B6030B000852311 /* HapticMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticMessage.swift; sourceTree = ""; };
2298C2041EE47DC900E2DDC1 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; };
2298C2061EE480D000E2DDC1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = ""; };
2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBottomAnimation.swift; sourceTree = ""; };
229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extensions.swift"; sourceTree = ""; };
22A2EA6E24EC6CFA00BB2540 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Extensions.swift"; sourceTree = ""; };
22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = CenteredView.xib; path = Resources/CenteredView.xib; sourceTree = ""; };
22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhysicsPanHandler.swift; sourceTree = ""; };
22E01F631E74EC8B00ACE19A /* MaskingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaskingView.swift; sourceTree = ""; };
22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibleMessage.swift; sourceTree = ""; };
22F27950210CE25900273E7F /* CornerRoundingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRoundingView.swift; sourceTree = ""; };
862C0C6A1D58E93300D06168 /* SwiftMessages.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = SwiftMessages.podspec; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
862C0CB01D5911C100D06168 /* NSBundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSBundle+Extensions.swift"; sourceTree = ""; };
862C0CDA1D5A397F00D06168 /* MessageView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MessageView.xib; path = Resources/MessageView.xib; sourceTree = ""; };
862C0CDB1D5A397F00D06168 /* StatusLine.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = StatusLine.xib; path = Resources/StatusLine.xib; sourceTree = ""; };
864495551D4F7C390056EB2A /* Identifiable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; };
864495581D4FA0AD0056EB2A /* SwiftMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftMessages.swift; sourceTree = ""; };
8644955C1D4FAF7C0056EB2A /* WindowViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowViewController.swift; sourceTree = ""; };
86589D461D64B6E40041676C /* BaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; };
86589D901D692B1B0041676C /* TabView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = TabView.xib; path = Resources/TabView.xib; sourceTree = ""; };
867BED201D622793005212E3 /* BackgroundViewable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundViewable.swift; sourceTree = ""; };
867E21821D4D025200594A41 /* MessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; };
867E21931D4D50BB00594A41 /* Presenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Presenter.swift; sourceTree = ""; };
86AAF8171D54F0650031EE32 /* PassthroughView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassthroughView.swift; sourceTree = ""; };
86AAF8191D54F0850031EE32 /* PassthroughWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassthroughWindow.swift; sourceTree = ""; };
86AAF81B1D551FE60031EE32 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; };
86AAF81D1D5549680031EE32 /* MarginAdjustable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarginAdjustable.swift; sourceTree = ""; };
86AAF82A1D580DD70031EE32 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; };
86AAF82C1D580F410031EE32 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
86B48AEC1D5A41C900063E2B /* SwiftMessages.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftMessages.framework; sourceTree = BUILT_PRODUCTS_DIR; };
86B48AEE1D5A41C900063E2B /* SwiftMessages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftMessages.h; sourceTree = ""; };
86B48AF01D5A41C900063E2B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
86B48AF51D5A41C900063E2B /* SwiftMessagesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftMessagesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
86B48AFA1D5A41C900063E2B /* SwiftMessagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMessagesTests.swift; sourceTree = ""; };
86B48AFC1D5A41C900063E2B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
86BBA8F81D5E01FC00FE8F16 /* CardView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = CardView.xib; path = Resources/CardView.xib; sourceTree = ""; };
B0E55A652DD110DB003D97B1 /* SwiftMessagesHideAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMessagesHideAction.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
86B48AE81D5A41C900063E2B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
86B48AF21D5A41C900063E2B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
86B48AF61D5A41C900063E2B /* SwiftMessages.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
220D38672597A94C00BB2B88 /* Extensions */ = {
isa = PBXGroup;
children = (
224FB69821153B440081D4DE /* CALayer+Extensions.swift */,
862C0CB01D5911C100D06168 /* NSBundle+Extensions.swift */,
86AAF81B1D551FE60031EE32 /* UIViewController+Extensions.swift */,
225304612290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift */,
220D386D2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift */,
);
name = Extensions;
sourceTree = "";
};
220D38682597A9FD00BB2B88 /* Extensions */ = {
isa = PBXGroup;
children = (
220655111FAF82B600F4E00F /* MarginAdjustable+Extensions.swift */,
22774B9F20B5EF2A00813732 /* UIEdgeInsets+Extensions.swift */,
229F778025FAB1E9008C2ACB /* UIWindow+Extensions.swift */,
22D3B4552B1CEF76002D8665 /* Task+Extensions.swift */,
);
name = Extensions;
sourceTree = "";
};
2244656C1EF1D62700C50413 /* Animations */ = {
isa = PBXGroup;
children = (
2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */,
2270044A1FAFA6DD0045DDC3 /* PhysicsAnimation.swift */,
22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */,
);
name = Animations;
sourceTree = "";
};
224FB6C8211651D10081D4DE /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "";
};
228F7DDA2ACF7029006C9644 /* SwiftUI */ = {
isa = PBXGroup;
children = (
B0E55A652DD110DB003D97B1 /* SwiftMessagesHideAction.swift */,
223DE69C2C29E50B000161E5 /* MessageGeometryProxy.swift */,
228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */,
228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */,
228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */,
);
name = SwiftUI;
sourceTree = "";
};
22D4779B20BF1C54005D0D71 /* View Controllers */ = {
isa = PBXGroup;
children = (
227BA6D820BF224A00E5A843 /* SwiftMessagesSegue.swift */,
);
name = "View Controllers";
sourceTree = "";
};
862C0CD81D5A396900D06168 /* Resources */ = {
isa = PBXGroup;
children = (
862C0CDA1D5A397F00D06168 /* MessageView.xib */,
86BBA8F81D5E01FC00FE8F16 /* CardView.xib */,
86589D901D692B1B0041676C /* TabView.xib */,
862C0CDB1D5A397F00D06168 /* StatusLine.xib */,
22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */,
228DF52B1FAD0802004F8A39 /* errorIconLight.png */,
228DF53E1FAD0804004F8A39 /* errorIconLight@2x.png */,
228DF5391FAD0804004F8A39 /* errorIconLight@3x.png */,
228DF5371FAD0803004F8A39 /* errorIconSubtle.png */,
228DF5431FAD0805004F8A39 /* errorIconSubtle@2x.png */,
228DF5451FAD0805004F8A39 /* errorIconSubtle@3x.png */,
228DF52F1FAD0803004F8A39 /* infoIcon.png */,
228DF52C1FAD0803004F8A39 /* infoIcon@2x.png */,
228DF5401FAD0804004F8A39 /* infoIcon@3x.png */,
228DF5411FAD0804004F8A39 /* infoIconLight.png */,
228DF5301FAD0803004F8A39 /* infoIconLight@2x.png */,
228DF5461FAD0805004F8A39 /* infoIconLight@3x.png */,
228DF5471FAD0805004F8A39 /* infoIconSubtle.png */,
228DF5421FAD0804004F8A39 /* infoIconSubtle@2x.png */,
228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */,
228DF5311FAD0803004F8A39 /* successIcon.png */,
228DF5321FAD0803004F8A39 /* successIcon@2x.png */,
228DF5331FAD0803004F8A39 /* successIcon@3x.png */,
228DF5481FAD0805004F8A39 /* successIconLight.png */,
228DF5441FAD0805004F8A39 /* successIconLight@2x.png */,
228DF5291FAD0802004F8A39 /* successIconLight@3x.png */,
228DF5351FAD0803004F8A39 /* successIconSubtle.png */,
228DF5381FAD0803004F8A39 /* successIconSubtle@2x.png */,
228DF52E1FAD0803004F8A39 /* successIconSubtle@3x.png */,
228DF53B1FAD0804004F8A39 /* warningIcon.png */,
228DF5341FAD0803004F8A39 /* warningIcon@2x.png */,
228DF53F1FAD0804004F8A39 /* warningIcon@3x.png */,
228DF52A1FAD0802004F8A39 /* warningIconLight.png */,
228DF53C1FAD0804004F8A39 /* warningIconLight@2x.png */,
228DF53A1FAD0804004F8A39 /* warningIconLight@3x.png */,
228DF52D1FAD0803004F8A39 /* warningIconSubtle.png */,
228DF5361FAD0803004F8A39 /* warningIconSubtle@2x.png */,
228DF53D1FAD0804004F8A39 /* warningIconSubtle@3x.png */,
228DF5231FACAC51004F8A39 /* errorIcon.png */,
228DF5241FACAC51004F8A39 /* errorIcon@2x.png */,
228DF5251FACAC51004F8A39 /* errorIcon@3x.png */,
);
name = Resources;
sourceTree = "";
};
864495571D4F7C490056EB2A /* Base */ = {
isa = PBXGroup;
children = (
22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */,
2298C2061EE480D000E2DDC1 /* Animator.swift */,
867BED201D622793005212E3 /* BackgroundViewable.swift */,
86589D461D64B6E40041676C /* BaseView.swift */,
22F27950210CE25900273E7F /* CornerRoundingView.swift */,
86AAF82A1D580DD70031EE32 /* Error.swift */,
22982C162B6030B000852311 /* HapticMessage.swift */,
864495551D4F7C390056EB2A /* Identifiable.swift */,
225304652293000C00A03ACF /* KeyboardTrackingView.swift */,
86AAF81D1D5549680031EE32 /* MarginAdjustable.swift */,
86AAF82C1D580F410031EE32 /* Theme.swift */,
224C3C922C28BC4400B50B18 /* TopBottomAnimationStyle.swift */,
224C3C8F2C28A2F900B50B18 /* TopBottomPresentable.swift */,
2298C2041EE47DC900E2DDC1 /* Weak.swift */,
0797E40D26EE12B400691606 /* WindowScene.swift */,
8644955C1D4FAF7C0056EB2A /* WindowViewController.swift */,
);
name = Base;
sourceTree = "";
};
867E21461D4D01D500594A41 = {
isa = PBXGroup;
children = (
86B48AED1D5A41C900063E2B /* SwiftMessages */,
86B48AF91D5A41C900063E2B /* SwiftMessagesTests */,
867E21501D4D01D500594A41 /* Products */,
224FB6C8211651D10081D4DE /* Frameworks */,
22A2EA6E24EC6CFA00BB2540 /* Package.swift */,
862C0C6A1D58E93300D06168 /* SwiftMessages.podspec */,
);
sourceTree = "";
};
867E21501D4D01D500594A41 /* Products */ = {
isa = PBXGroup;
children = (
86B48AEC1D5A41C900063E2B /* SwiftMessages.framework */,
86B48AF51D5A41C900063E2B /* SwiftMessagesTests.xctest */,
);
name = Products;
sourceTree = "";
};
867E218E1D4D3DFD00594A41 /* Internal */ = {
isa = PBXGroup;
children = (
867E21931D4D50BB00594A41 /* Presenter.swift */,
86AAF8171D54F0650031EE32 /* PassthroughView.swift */,
22E01F631E74EC8B00ACE19A /* MaskingView.swift */,
86AAF8191D54F0850031EE32 /* PassthroughWindow.swift */,
220D38672597A94C00BB2B88 /* Extensions */,
);
name = Internal;
sourceTree = "";
};
86B48AED1D5A41C900063E2B /* SwiftMessages */ = {
isa = PBXGroup;
children = (
864495581D4FA0AD0056EB2A /* SwiftMessages.swift */,
867E21821D4D025200594A41 /* MessageView.swift */,
862C0CD81D5A396900D06168 /* Resources */,
2244656C1EF1D62700C50413 /* Animations */,
22D4779B20BF1C54005D0D71 /* View Controllers */,
228F7DDA2ACF7029006C9644 /* SwiftUI */,
864495571D4F7C490056EB2A /* Base */,
220D38682597A9FD00BB2B88 /* Extensions */,
867E218E1D4D3DFD00594A41 /* Internal */,
86B48B031D5A41E500063E2B /* Support */,
);
path = SwiftMessages;
sourceTree = "";
};
86B48AF91D5A41C900063E2B /* SwiftMessagesTests */ = {
isa = PBXGroup;
children = (
86B48AFA1D5A41C900063E2B /* SwiftMessagesTests.swift */,
86B48AFC1D5A41C900063E2B /* Info.plist */,
);
path = SwiftMessagesTests;
sourceTree = "";
};
86B48B031D5A41E500063E2B /* Support */ = {
isa = PBXGroup;
children = (
86B48AEE1D5A41C900063E2B /* SwiftMessages.h */,
86B48AF01D5A41C900063E2B /* Info.plist */,
);
name = Support;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
86B48AE91D5A41C900063E2B /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
86B48AEF1D5A41C900063E2B /* SwiftMessages.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
86B48AEB1D5A41C900063E2B /* SwiftMessages */ = {
isa = PBXNativeTarget;
buildConfigurationList = 86B48AFD1D5A41C900063E2B /* Build configuration list for PBXNativeTarget "SwiftMessages" */;
buildPhases = (
86B48AE71D5A41C900063E2B /* Sources */,
86B48AE81D5A41C900063E2B /* Frameworks */,
86B48AE91D5A41C900063E2B /* Headers */,
86B48AEA1D5A41C900063E2B /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = SwiftMessages;
productName = SwiftMessages;
productReference = 86B48AEC1D5A41C900063E2B /* SwiftMessages.framework */;
productType = "com.apple.product-type.framework";
};
86B48AF41D5A41C900063E2B /* SwiftMessagesTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 86B48B001D5A41C900063E2B /* Build configuration list for PBXNativeTarget "SwiftMessagesTests" */;
buildPhases = (
86B48AF11D5A41C900063E2B /* Sources */,
86B48AF21D5A41C900063E2B /* Frameworks */,
86B48AF31D5A41C900063E2B /* Resources */,
);
buildRules = (
);
dependencies = (
86B48AF81D5A41C900063E2B /* PBXTargetDependency */,
);
name = SwiftMessagesTests;
productName = SwiftMessagesTests;
productReference = 86B48AF51D5A41C900063E2B /* SwiftMessagesTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
867E21471D4D01D500594A41 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0730;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "SwiftKick Mobile";
TargetAttributes = {
86B48AEB1D5A41C900063E2B = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
86B48AF41D5A41C900063E2B = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 0900;
};
};
};
buildConfigurationList = 867E214A1D4D01D500594A41 /* Build configuration list for PBXProject "SwiftMessages" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 867E21461D4D01D500594A41;
productRefGroup = 867E21501D4D01D500594A41 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
86B48AEB1D5A41C900063E2B /* SwiftMessages */,
86B48AF41D5A41C900063E2B /* SwiftMessagesTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
86B48AEA1D5A41C900063E2B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
228DF5581FAD0806004F8A39 /* errorIconSubtle.png in Resources */,
228DF55C1FAD0806004F8A39 /* warningIcon.png in Resources */,
228DF5521FAD0806004F8A39 /* successIcon.png in Resources */,
228DF5531FAD0806004F8A39 /* successIcon@2x.png in Resources */,
228DF54C1FAD0806004F8A39 /* errorIconLight.png in Resources */,
228DF55B1FAD0806004F8A39 /* warningIconLight@3x.png in Resources */,
228DF5571FAD0806004F8A39 /* warningIconSubtle@2x.png in Resources */,
228DF5651FAD0806004F8A39 /* successIconLight@2x.png in Resources */,
228DF5671FAD0806004F8A39 /* infoIconLight@3x.png in Resources */,
E6E49F911D70A344006CB883 /* MessageView.xib in Resources */,
228DF5551FAD0806004F8A39 /* warningIcon@2x.png in Resources */,
228DF5661FAD0806004F8A39 /* errorIconSubtle@3x.png in Resources */,
228DF5511FAD0806004F8A39 /* infoIconLight@2x.png in Resources */,
228DF5611FAD0806004F8A39 /* infoIcon@3x.png in Resources */,
228DF54A1FAD0806004F8A39 /* successIconLight@3x.png in Resources */,
228DF5271FACAC51004F8A39 /* errorIcon@2x.png in Resources */,
228DF55D1FAD0806004F8A39 /* warningIconLight@2x.png in Resources */,
228DF5501FAD0806004F8A39 /* infoIcon.png in Resources */,
228DF54E1FAD0806004F8A39 /* warningIconSubtle.png in Resources */,
228DF5601FAD0806004F8A39 /* warningIcon@3x.png in Resources */,
228DF5641FAD0806004F8A39 /* errorIconSubtle@2x.png in Resources */,
228DF55F1FAD0806004F8A39 /* errorIconLight@2x.png in Resources */,
228DF55A1FAD0806004F8A39 /* errorIconLight@3x.png in Resources */,
228DF5631FAD0806004F8A39 /* infoIconSubtle@2x.png in Resources */,
86BBA8F91D5E01FC00FE8F16 /* CardView.xib in Resources */,
228DF5681FAD0806004F8A39 /* infoIconSubtle.png in Resources */,
228DF54F1FAD0806004F8A39 /* successIconSubtle@3x.png in Resources */,
228DF56A1FAD0806004F8A39 /* infoIconSubtle@3x.png in Resources */,
228DF5261FACAC51004F8A39 /* errorIcon.png in Resources */,
228DF5691FAD0806004F8A39 /* successIconLight.png in Resources */,
86589D911D692B1C0041676C /* TabView.xib in Resources */,
228DF5621FAD0806004F8A39 /* infoIconLight.png in Resources */,
228DF55E1FAD0806004F8A39 /* warningIconSubtle@3x.png in Resources */,
228DF5591FAD0806004F8A39 /* successIconSubtle@2x.png in Resources */,
E6E49F921D70A349006CB883 /* StatusLine.xib in Resources */,
228DF5561FAD0806004F8A39 /* successIconSubtle.png in Resources */,
228DF5541FAD0806004F8A39 /* successIcon@3x.png in Resources */,
228DF54B1FAD0806004F8A39 /* warningIconLight.png in Resources */,
228DF54D1FAD0806004F8A39 /* infoIcon@2x.png in Resources */,
22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */,
228DF5281FACAC51004F8A39 /* errorIcon@3x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
86B48AF31D5A41C900063E2B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
86B48AE71D5A41C900063E2B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
225304662293000C00A03ACF /* KeyboardTrackingView.swift in Sources */,
22774BA020B5EF2A00813732 /* UIEdgeInsets+Extensions.swift in Sources */,
86BBA8FC1D5E03F100FE8F16 /* MessageView.swift in Sources */,
86BBA9061D5E040C00FE8F16 /* Identifiable.swift in Sources */,
22F27951210CE25900273E7F /* CornerRoundingView.swift in Sources */,
86BBA9011D5E040600FE8F16 /* PassthroughWindow.swift in Sources */,
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */,
22D3B4562B1CEF76002D8665 /* Task+Extensions.swift in Sources */,
22982C172B6030B000852311 /* HapticMessage.swift in Sources */,
86BBA9031D5E040600FE8F16 /* UIViewController+Extensions.swift in Sources */,
228F7DDF2ACF703A006C9644 /* SwiftMessageModifier.swift in Sources */,
224FB69921153B440081D4DE /* CALayer+Extensions.swift in Sources */,
22E01F641E74EC8B00ACE19A /* MaskingView.swift in Sources */,
224C3C932C28BC4900B50B18 /* TopBottomAnimationStyle.swift in Sources */,
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */,
228F7DE02ACF703A006C9644 /* MessageViewConvertible.swift in Sources */,
86BBA9001D5E040600FE8F16 /* PassthroughView.swift in Sources */,
22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */,
227BA6D920BF224A00E5A843 /* SwiftMessagesSegue.swift in Sources */,
220655121FAF82B600F4E00F /* MarginAdjustable+Extensions.swift in Sources */,
228F7DDE2ACF703A006C9644 /* MessageHostingView.swift in Sources */,
22E307FF1E74C5B100E35893 /* AccessibleMessage.swift in Sources */,
220D386E2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift in Sources */,
2270044B1FAFA6DD0045DDC3 /* PhysicsAnimation.swift in Sources */,
224C3C902C28A2F900B50B18 /* TopBottomPresentable.swift in Sources */,
B0E55A662DD110EA003D97B1 /* SwiftMessagesHideAction.swift in Sources */,
86BBA9041D5E040600FE8F16 /* NSBundle+Extensions.swift in Sources */,
86BBA8FD1D5E03F800FE8F16 /* SwiftMessages.swift in Sources */,
86BBA9021D5E040600FE8F16 /* WindowViewController.swift in Sources */,
229F778125FAB1E9008C2ACB /* UIWindow+Extensions.swift in Sources */,
86BBA8FF1D5E040600FE8F16 /* Presenter.swift in Sources */,
86BBA9051D5E040C00FE8F16 /* Theme.swift in Sources */,
86BBA9081D5E040C00FE8F16 /* Error.swift in Sources */,
2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */,
86589D471D64B6E40041676C /* BaseView.swift in Sources */,
0797E40E26EE12B400691606 /* WindowScene.swift in Sources */,
225304622290C76E00A03ACF /* NSLayoutConstraint+Extensions.swift in Sources */,
223DE69D2C29E50C000161E5 /* MessageGeometryProxy.swift in Sources */,
86BBA9071D5E040C00FE8F16 /* MarginAdjustable.swift in Sources */,
867BED211D622793005212E3 /* BackgroundViewable.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
86B48AF11D5A41C900063E2B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
86B48AFB1D5A41C900063E2B /* SwiftMessagesTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
86B48AF81D5A41C900063E2B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 86B48AEB1D5A41C900063E2B /* SwiftMessages */;
targetProxy = 86B48AF71D5A41C900063E2B /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
867E21751D4D01D500594A41 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
};
name = Debug;
};
867E21761D4D01D500594A41 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 3.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
86B48AFE1D5A41C900063E2B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES;
INFOPLIST_FILE = SwiftMessages/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
WARNING_CFLAGS = "";
};
name = Debug;
};
86B48AFF1D5A41C900063E2B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES;
INFOPLIST_FILE = SwiftMessages/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
WARNING_CFLAGS = "";
};
name = Release;
};
86B48B011D5A41C900063E2B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = SwiftMessagesTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessagesTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
86B48B021D5A41C900063E2B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = SwiftMessagesTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessagesTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
867E214A1D4D01D500594A41 /* Build configuration list for PBXProject "SwiftMessages" */ = {
isa = XCConfigurationList;
buildConfigurations = (
867E21751D4D01D500594A41 /* Debug */,
867E21761D4D01D500594A41 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
86B48AFD1D5A41C900063E2B /* Build configuration list for PBXNativeTarget "SwiftMessages" */ = {
isa = XCConfigurationList;
buildConfigurations = (
86B48AFE1D5A41C900063E2B /* Debug */,
86B48AFF1D5A41C900063E2B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
86B48B001D5A41C900063E2B /* Build configuration list for PBXNativeTarget "SwiftMessagesTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
86B48B011D5A41C900063E2B /* Debug */,
86B48B021D5A41C900063E2B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 867E21471D4D01D500594A41 /* Project object */;
}
================================================
FILE: SwiftMessages.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: SwiftMessages.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: SwiftMessages.xcodeproj/xcshareddata/xcschemes/SwiftMessages.xcscheme
================================================
================================================
FILE: SwiftMessagesTests/Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
BNDL
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1
================================================
FILE: SwiftMessagesTests/SwiftMessagesTests.swift
================================================
//
// SwiftMessagesTests.swift
// SwiftMessagesTests
//
// Created by Timothy Moose on 8/9/16.
// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved.
//
import XCTest
@testable import SwiftMessages
class SwiftMessagesTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Demo message background.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xEB",
"green" : "0xE1",
"red" : "0xAC"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/DemoMessage.swift
================================================
//
// DemoMessage.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
import SwiftMessages
struct DemoMessage: Identifiable {
let title: String
let body: String
let style: DemoMessageView.Style
var id: String { title + body }
}
extension DemoMessage: MessageViewConvertible {
func asMessageView() -> DemoMessageView {
DemoMessageView(message: self, style: style)
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift
================================================
//
// DemoMessageView.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
// A message view with a title and message.
struct DemoMessageView: View {
// MARK: - API
enum Style {
case standard
case card
case tab
}
let message: DemoMessage
let style: Style
// MARK: - Variables
// MARK: - Constants
// MARK: - Body
var body: some View {
switch style {
case .standard:
content()
// Mask the content and extend background into the safe area.
.mask {
Rectangle()
.edgesIgnoringSafeArea(.top)
}
case .card:
content()
// Mask the content with a rounded rectangle
.mask {
RoundedRectangle(cornerRadius: 15)
}
// External padding around the card
.padding(10)
case .tab:
content()
// Mask the content with rounded bottom edge and extend background into the safe area.
.mask {
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
.edgesIgnoringSafeArea(.top)
}
}
}
@ViewBuilder private func content() -> some View {
VStack(alignment: .leading) {
Text(message.title).font(.system(size: 20, weight: .bold))
Text(message.body)
}
.multilineTextAlignment(.leading)
// Internal padding of the card
.padding(30)
// Greedy width
.frame(maxWidth: .infinity)
.background(.demoMessageBackground)
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/DemoMessageWithButtonView.swift
================================================
//
// DemoMessageWithButtonView.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 1/15/24.
//
import SwiftUI
// A message view with a title, message and button.
struct DemoMessageWithButtonView: View where Button: View {
// MARK: - API
enum Style {
case standard
case card
case tab
}
init(message: DemoMessage, style: Style, @ViewBuilder button: @escaping () -> Button) {
self.message = message
self.style = style
self.button = button
}
// MARK: - Variables
let message: DemoMessage
let style: Style
@ViewBuilder let button: () -> Button
// MARK: - Constants
// MARK: - Body
var body: some View {
switch style {
case .standard:
content()
// Mask the content and extend background into the safe area.
.mask {
Rectangle()
.edgesIgnoringSafeArea(.top)
}
case .card:
content()
// Mask the content with a rounded rectangle
.mask {
RoundedRectangle(cornerRadius: 15)
}
// External padding around the card
.padding(10)
case .tab:
content()
// Mask the content with rounded bottom edge and extend background into the safe area.
.mask {
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
.edgesIgnoringSafeArea(.top)
}
}
}
@ViewBuilder private func content() -> some View {
VStack() {
Text(message.title).font(.system(size: 20, weight: .bold))
Text(message.body)
button()
}
.multilineTextAlignment(.center)
// Internal padding of the card
.padding(30)
// Greedy width
.frame(maxWidth: .infinity)
.background(.demoMessageBackground)
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/DemoView.swift
================================================
//
// DemoView.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
import SwiftMessages
struct DemoView: View {
/// Use this to manually hide the swift message
@Environment(\.swiftMessagesHide) private var hide
/// Demonstrates purely data-driven message presentation.
@State var message: DemoMessage?
/// Demonstrates message presentation with a view builder.
@State var messageWithButton: DemoMessage?
var body: some View {
VStack {
Button("Show standard message") {
message = DemoMessage(
title: "Demo",
body: "This is a sample SwiftUI card-style message! This content should be long enough to wrap.",
style: .standard
)
}
Button("Show card message") {
message = DemoMessage(
title: "Demo",
body: "This is a sample SwiftUI card-style message! This content should be long enough to wrap.",
style: .card
)
}
Button("Show tab message") {
message = DemoMessage(
title: "Demo",
body: "This is a sample SwiftUI card-style message! This content should be long enough to wrap.",
style: .tab
)
}
Button("Show message with button") {
messageWithButton = DemoMessage(
title: "Demo",
body: "This message view has a button was constructed with a view builder.",
style: .card
)
}
}
.buttonStyle(.bordered)
.swiftMessage(message: $message)
.swiftMessage(message: $messageWithButton) { message in
DemoMessageWithButtonView(message: message, style: .card) {
Button("Hide") {
hide(animated: true)
}
.buttonStyle(.bordered)
}
}
}
}
#Preview {
DemoView()
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Info.plist
================================================
================================================
FILE: SwiftUIDemo/SwiftUIDemo/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo/SwiftUIDemoApp.swift
================================================
//
// SwiftUIDemoApp.swift
// SwiftUIDemo
//
// Created by Timothy Moose on 10/5/23.
//
import SwiftUI
@main
struct SwiftUIDemoApp: App {
var body: some Scene {
WindowGroup {
DemoView()
}
}
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
22549DC02B55CFE8005E3E21 /* DemoMessageWithButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */; };
228F7DAD2ACF17E8006C9644 /* SwiftUIDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DAC2ACF17E8006C9644 /* SwiftUIDemoApp.swift */; };
228F7DAF2ACF17E8006C9644 /* DemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DAE2ACF17E8006C9644 /* DemoView.swift */; };
228F7DB12ACF17E9006C9644 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 228F7DB02ACF17E9006C9644 /* Assets.xcassets */; };
228F7DB42ACF17E9006C9644 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 228F7DB32ACF17E9006C9644 /* Preview Assets.xcassets */; };
228F7DC82ACF1E63006C9644 /* SwiftMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 228F7DC32ACF1E1E006C9644 /* SwiftMessages.framework */; };
228F7DC92ACF1E63006C9644 /* SwiftMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 228F7DC32ACF1E1E006C9644 /* SwiftMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
228F7DD52ACF59E4006C9644 /* DemoMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DD42ACF59E4006C9644 /* DemoMessage.swift */; };
228F7DD72ACF5C2E006C9644 /* DemoMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DD62ACF5C2E006C9644 /* DemoMessageView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
228F7DC22ACF1E1E006C9644 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 228F7DBD2ACF1E1E006C9644 /* SwiftMessages.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 86B48AEC1D5A41C900063E2B;
remoteInfo = SwiftMessages;
};
228F7DC42ACF1E1E006C9644 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 228F7DBD2ACF1E1E006C9644 /* SwiftMessages.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 86B48AF51D5A41C900063E2B;
remoteInfo = SwiftMessagesTests;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
228F7DCA2ACF1E63006C9644 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
228F7DC92ACF1E63006C9644 /* SwiftMessages.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoMessageWithButtonView.swift; sourceTree = ""; };
228F7DA92ACF17E8006C9644 /* SwiftUIDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
228F7DAC2ACF17E8006C9644 /* SwiftUIDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIDemoApp.swift; sourceTree = ""; };
228F7DAE2ACF17E8006C9644 /* DemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoView.swift; sourceTree = ""; };
228F7DB02ACF17E9006C9644 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
228F7DB32ACF17E9006C9644 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
228F7DBB2ACF1DB5006C9644 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
228F7DBD2ACF1E1E006C9644 /* SwiftMessages.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftMessages.xcodeproj; path = ../SwiftMessages.xcodeproj; sourceTree = ""; };
228F7DD42ACF59E4006C9644 /* DemoMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoMessage.swift; sourceTree = ""; };
228F7DD62ACF5C2E006C9644 /* DemoMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoMessageView.swift; sourceTree = ""; };
2291AA492AD1E3EC0084868E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; };
2291AA4C2AD1E4520084868E /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
228F7DA62ACF17E8006C9644 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
228F7DC82ACF1E63006C9644 /* SwiftMessages.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
228F7DA02ACF17E8006C9644 = {
isa = PBXGroup;
children = (
2291AA4C2AD1E4520084868E /* CHANGELOG.md */,
2291AA492AD1E3EC0084868E /* README.md */,
228F7DAB2ACF17E8006C9644 /* SwiftUIDemo */,
228F7DAA2ACF17E8006C9644 /* Products */,
228F7DC72ACF1E63006C9644 /* Frameworks */,
228F7DBD2ACF1E1E006C9644 /* SwiftMessages.xcodeproj */,
);
sourceTree = "";
};
228F7DAA2ACF17E8006C9644 /* Products */ = {
isa = PBXGroup;
children = (
228F7DA92ACF17E8006C9644 /* SwiftUIDemo.app */,
);
name = Products;
sourceTree = "";
};
228F7DAB2ACF17E8006C9644 /* SwiftUIDemo */ = {
isa = PBXGroup;
children = (
228F7DBB2ACF1DB5006C9644 /* Info.plist */,
228F7DAC2ACF17E8006C9644 /* SwiftUIDemoApp.swift */,
228F7DAE2ACF17E8006C9644 /* DemoView.swift */,
228F7DD42ACF59E4006C9644 /* DemoMessage.swift */,
228F7DD62ACF5C2E006C9644 /* DemoMessageView.swift */,
22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */,
228F7DB02ACF17E9006C9644 /* Assets.xcassets */,
228F7DB22ACF17E9006C9644 /* Preview Content */,
);
path = SwiftUIDemo;
sourceTree = "";
};
228F7DB22ACF17E9006C9644 /* Preview Content */ = {
isa = PBXGroup;
children = (
228F7DB32ACF17E9006C9644 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "";
};
228F7DBE2ACF1E1E006C9644 /* Products */ = {
isa = PBXGroup;
children = (
228F7DC32ACF1E1E006C9644 /* SwiftMessages.framework */,
228F7DC52ACF1E1E006C9644 /* SwiftMessagesTests.xctest */,
);
name = Products;
sourceTree = "";
};
228F7DC72ACF1E63006C9644 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
228F7DA82ACF17E8006C9644 /* SwiftUIDemo */ = {
isa = PBXNativeTarget;
buildConfigurationList = 228F7DB72ACF17E9006C9644 /* Build configuration list for PBXNativeTarget "SwiftUIDemo" */;
buildPhases = (
228F7DA52ACF17E8006C9644 /* Sources */,
228F7DA62ACF17E8006C9644 /* Frameworks */,
228F7DA72ACF17E8006C9644 /* Resources */,
228F7DCA2ACF1E63006C9644 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = SwiftUIDemo;
productName = SwiftUIDemo;
productReference = 228F7DA92ACF17E8006C9644 /* SwiftUIDemo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
228F7DA12ACF17E8006C9644 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1500;
TargetAttributes = {
228F7DA82ACF17E8006C9644 = {
CreatedOnToolsVersion = 15.0;
};
};
};
buildConfigurationList = 228F7DA42ACF17E8006C9644 /* Build configuration list for PBXProject "SwiftUIDemo" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 228F7DA02ACF17E8006C9644;
productRefGroup = 228F7DAA2ACF17E8006C9644 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 228F7DBE2ACF1E1E006C9644 /* Products */;
ProjectRef = 228F7DBD2ACF1E1E006C9644 /* SwiftMessages.xcodeproj */;
},
);
projectRoot = "";
targets = (
228F7DA82ACF17E8006C9644 /* SwiftUIDemo */,
);
};
/* End PBXProject section */
/* Begin PBXReferenceProxy section */
228F7DC32ACF1E1E006C9644 /* SwiftMessages.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = SwiftMessages.framework;
remoteRef = 228F7DC22ACF1E1E006C9644 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
228F7DC52ACF1E1E006C9644 /* SwiftMessagesTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = SwiftMessagesTests.xctest;
remoteRef = 228F7DC42ACF1E1E006C9644 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
228F7DA72ACF17E8006C9644 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
228F7DB42ACF17E9006C9644 /* Preview Assets.xcassets in Resources */,
228F7DB12ACF17E9006C9644 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
228F7DA52ACF17E8006C9644 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
228F7DD52ACF59E4006C9644 /* DemoMessage.swift in Sources */,
228F7DD72ACF5C2E006C9644 /* DemoMessageView.swift in Sources */,
228F7DAF2ACF17E8006C9644 /* DemoView.swift in Sources */,
228F7DAD2ACF17E8006C9644 /* SwiftUIDemoApp.swift in Sources */,
22549DC02B55CFE8005E3E21 /* DemoMessageWithButtonView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
228F7DB52ACF17E9006C9644 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
228F7DB62ACF17E9006C9644 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
228F7DB82ACF17E9006C9644 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUIDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SwiftUIDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.swiftkickmobile.SwiftMessages.SwiftUIDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
228F7DB92ACF17E9006C9644 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"SwiftUIDemo/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SwiftUIDemo/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.swiftkickmobile.SwiftMessages.SwiftUIDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
228F7DA42ACF17E8006C9644 /* Build configuration list for PBXProject "SwiftUIDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
228F7DB52ACF17E9006C9644 /* Debug */,
228F7DB62ACF17E9006C9644 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
228F7DB72ACF17E9006C9644 /* Build configuration list for PBXNativeTarget "SwiftUIDemo" */ = {
isa = XCConfigurationList;
buildConfigurations = (
228F7DB82ACF17E9006C9644 /* Debug */,
228F7DB92ACF17E9006C9644 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 228F7DA12ACF17E8006C9644 /* Project object */;
}
================================================
FILE: SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: ViewControllers.md
================================================
# View Controllers
[`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) is a configurable subclass of `UIStoryboardSegue` that presents and dismisses modal view controllers by acting as the presenting view controller's `transitioningDelegate` and utilizing SwiftMessages' `show()` and `hide()` function on the destination view controller's view.
## Usage
### Interface Builder
First, create a segue by control-dragging from the sender element to the destination view controller. Then select "swift messages" (or the name of a `SwiftMessagesSegue` subclass) in the segue type prompt. In the image below, we've created a segue using the `VeryNiceSegue` subclass by selecting "very nice" from the prompt.
### Programatic
`SwiftMessagesSegue` can be used without an associated storyboard or segue by doing the following in the presenting view controller.
````swift
let destinationVC = ... // make a reference to a destination view controller
let segue = SwiftMessagesSegue(identifier: nil, source: self, destination: destinationVC)
... // do any configuration here
segue.perform()
````
To dismiss, call the UIKit API on the presenting view controller:
````swift
dismiss(animated: true, completion: nil)
````
It is not necessary to retain `segue` because it retains itself until dismissal. However, you can retain it if you plan to `perform()` more than once.
#### Present the controller on top of all controllers
If you don't know the presenter or you don't want to pass it as a source, like when you
have a completely separated message controller, you can pass a `WindowViewController`
as the `source` argument of the segue's initializer.
By default, the window will be shown in the current window scene at `.normal` window level.
However, these parameters can be customized by initializing the view controller with a `SwiftMessages.Config` that has the `SwiftMessages.Config.presentationContext` set to either `.window` or `.windowScene`:
```swift
let destinationVC = ... // make a reference to a destination view controller
var config = SwiftMessages.defaultConfig
config.presentationContext = .windowScene(...) // specify the window properties
let sourceVC = WindowViewController(config: config)
let segue = SwiftMessagesSegue(identifier: nil, source: self, destination: destinationVC)
segue.perform()
```
### Configuration
`SwiftMessagesSegue` generally requires configuration to achieve specific layouts and optional behaviors. There are a few good ways to do this:
1. __(Recommended)__ Subclass `SwiftMessagesSegue` and apply configurations in `init(identifier:source:destination:)`. Subclasses will automatically appear in the segue type dialog using an auto-generated name. For example, the name for "VeryNiceSegue" would be "very nice".
```swift
class VeryNiceSegue: SwiftMessagesSegue {
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
configure(layout: .bottomCard)
dimMode = .blur(style: .dark, alpha: 0.9, interactive: true)
messageView.configureNoDropShadow()
}
}
```
1. Apply configurations in `prepare(for:sender:)` of the presenting view controller after down-casting the segue to `SwiftMessagesSegue`.
````swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let segue = segue as? SwiftMessagesSegue {
segue.configure(layout: .bottomCard)
segue.dimMode = .blur(style: .dark, alpha: 0.9, interactive: true)
segue.messageView.configureNoDropShadow()
}
}
````
The `configure(layout:)` method is a shortcut for configuring some basic layout and animation options that roughly mirror the options in `SwiftMessages.Layout`.
````swift
// Configure a bottom card-style presentation
segue.configure(layout: .bottomCard)
````
Many more styles can be achieved by customizing the underlying options. Some of the `SwiftMessages.Config` options are useful for view controller presentation and are mirrored on `SwiftMessagesSegue`.
````swift
// Turn off interactive dismiss
segue.interactiveHide = false
// Enable dimmed background with tap-to-dismiss
segue.dimMode = .gray(interactive: true)
// Specify the animation and positioning
segue.presentationStyle = .bottom
````
The `messageView` property provides access to an instance of [`BaseView`](./SwiftMessages/BaseView.swift), the superclass of [`MessageView`](./SwiftMessages/MessageView.swift), that serves as the view presented by SwiftMessages. The view controller's view is contained as a descendant of this view. There are some useful options available on `messageView`:
````swift
// Increase the internal layout margins. With the `.background` containment option,
// the margin additions specify the outer margins around `messageView.backgroundView`.
segue.messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
// Collapse layout margin edges that encroach on non-zero safe area insets.
messageView.collapseLayoutMarginAdditions = true
// Add a default drop shadow.
segue.messageView.configureDropShadow()
// Indicate that the view controller's view should be installed
// as the `backgroundView` of `messageView`.
segue.containment = .background
````
The view controller's view is a direct subview of `containerView`, an instance of [`ViewControllerContainerView`](./SwiftMessages/ViewControllerContainerView.swift), which provides corner rounding options.
````swift
// Change the corner radius
segue.containerView.cornerRadius = 20
````
### Sizing
`SwiftMessagesSegue` provides default view controller sizing based on device, with width on iPad being limited to 500pt max. However, it is recommended that you explicitly specify size appropriate for your content using one of the following methods.
1. Define sufficient width and height constraints in your view controller such that it sizes itself.
1. Set the `preferredContentSize` property (a.k.a "Use Preferred Explicit Size" in Interface Builder's attribute inspector). Zeros are ignored, e.g. `CGSize(width: 0, height: 350)` only affects the height.
1. Add explicit width and/or height constraints to `segue.messageView.backgroundView`.
Note that `Layout.topMessage` and `Layout.bottomMessage` are always full screen width. For other layouts, the there is a maximum 500pt width on for regular horizontal size class (iPad) at 950 priority. This limit can be overridden by adding higher-priority constraints.
### Keyboard Avoidance
The `KeyboardTrackingView` class can be used to cause the message view to avoid the keyboard by sliding up when the keyboard gets too close.
````swift
segue.keyboardTrackingView = KeyboardTrackingView()
````
You can incorporate `KeyboardTrackingView` into your app even when you're not using SwiftMessages. Install into your view hierarchy by pinning `KeyboardTrackingView` to the bottom, leading, and trailing edges of the screen. Then pin the bottom of your content that should avoid the keyboard to the top `KeyboardTrackingView`. Use an equality constraint to strictly track the keyboard or an inequality constraint to only move when the keyboard gets too close. `KeyboardTrackingView` works by observing keyboard notifications and adjusting its height to maintain its top edge above the keyboard, thereby pushing your content up. See the comments in `KeyboardTrackingView` for configuration options.
See [`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) for additional documentation and technical details.
================================================
FILE: iMessageDemo/.Podfile
================================================
target 'iMessageDemo' do
use_frameworks!
workspace 'iMessageDemo.xcworkspace'
xcodeproj 'iMessageDemo.xcodeproj'
end
target 'iMessageExtensionDemo' do
use_frameworks!
workspace 'iMessageDemo.xcworkspace'
xcodeproj 'iMessageDemo.xcodeproj'
pod 'SwiftMessages/AppExtension', :path => '../'
end
================================================
FILE: iMessageDemo/Podfile
================================================
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
install! 'cocoapods',
:share_schemes_for_development_pods => false
target 'iMessageDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for iMessageDemo
end
target 'iMessageExtensionDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for iMessageExtensionDemo
pod 'SwiftMessages/AppExtension', :path => "../"
end
post_install do |installer|
# Blow away schemes – the schemes created by CocoaPods break Carthage builds
# because they incluede a SwiftMessages framework that Carthage picks
# over the main SwiftMessages framework. The SwiftMessages framework that gets
# picked is configured for an app extension and doesn't work correctly in an app.
File.directory?(path)
[
"#{installer.sandbox.root}/Pods.xcodeproj/xcuserdata",
"#{installer.sandbox.root}/Pods.xcodeproj/xcshareddata"
].each { |path|
if File.directory?(path)
FileUtils.remove_dir(path)
end
}
end
================================================
FILE: iMessageDemo/Pods/Local Podspecs/SwiftMessages.podspec.json
================================================
{
"name": "SwiftMessages",
"version": "10.0.1",
"license": {
"type": "MIT"
},
"homepage": "https://github.com/SwiftKickMobile/SwiftMessages",
"authors": {
"Timothy Moose": "tim@swiftkickmobile.com"
},
"summary": "A very flexible message bar for iOS written in Swift.",
"source": {
"git": "https://github.com/SwiftKickMobile/SwiftMessages.git",
"tag": "10.0.1"
},
"platforms": {
"ios": "13.0"
},
"swift_versions": "5.0",
"frameworks": "UIKit",
"requires_arc": true,
"default_subspecs": "App",
"subspecs": [
{
"name": "App",
"source_files": "SwiftMessages/**/*.swift",
"resource_bundles": {
"SwiftMessages": [
"SwiftMessages/Resources/*.*"
]
}
},
{
"name": "AppExtension",
"source_files": "SwiftMessages/**/*.swift",
"exclude_files": "SwiftMessages/**/SegueConvenienceClasses.swift",
"resource_bundles": {
"SwiftMessages_SwiftMessages": [
"SwiftMessages/Resources/**/*.*"
]
},
"pod_target_xcconfig": {
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": "SWIFTMESSAGES_APP_EXTENSIONS",
"GCC_PREPROCESSOR_DEFINITIONS": "SWIFTMESSAGES_APP_EXTENSIONS=1"
}
}
],
"swift_version": "5.0"
}
================================================
FILE: iMessageDemo/Pods/Pods.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
00A4A53E0D1EFC70DA1EFAEFA82C4BA1 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 442F9FAEDF369CADB9E6E8FF3668FD95 /* Weak.swift */; };
03C7FA1BE62CCD280D63F4C7CDDFB27F /* AccessibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E8A4D41E0A0E52C839F7197C4CE2A0 /* AccessibleMessage.swift */; };
04BCE3C12FDE7E91C9EF1FE154E80E2B /* SwiftMessageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19DACBEC8DD85FB850D299DFB7A0374 /* SwiftMessageModifier.swift */; };
04C771EB4784B94AC6CA28ED6A919D50 /* WindowScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB9866F195EB0AED31A33AEC27B67C1 /* WindowScene.swift */; };
096A6663757CB52C8A34B3E3146279C8 /* infoIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 2CF5DC74916EBE02E70234620CD80150 /* infoIcon@2x.png */; };
0ADA608E3B9307694DCF4D0BC426E7FE /* warningIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B241F1B5D295E7806363B802DF03A085 /* warningIcon@2x.png */; };
0C093AF5F4D753325F4CE92F2F943CB4 /* MarginAdjustable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DF8F43552C4F1C4CAEFBAFD9A77004 /* MarginAdjustable.swift */; };
0D53997CCCD141D2303E87C29CEFE211 /* MessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = A4D741EF8B40F5472CE2C1C1A34426BD /* MessageView.xib */; };
1441D5A06DD6903562EB794FC1AE8822 /* TabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C33B3DA756F19F5C93D498D557BEB820 /* TabView.xib */; };
158EF18377B720081C31C2F2100DDA7A /* Presenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D18688EA04EA2F2BC48ED05D1FE5404 /* Presenter.swift */; };
19C0F7897044E72D3A13DE703920FFE1 /* MessageHostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F989D33CAC3BCDA9B3C4B18F23A4DBE /* MessageHostingView.swift */; };
1E507663F3C402CBCA0C50A876F472EF /* errorIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D6A205E933052E98349DC710D78304CB /* errorIconLight@3x.png */; };
205A52121D20436063240893A461B29C /* errorIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 38D28C78FCAE6706F1294CEFA6B25F88 /* errorIconSubtle@2x.png */; };
24DA4886290040A0EC88B63D82EF412D /* warningIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5E02B1554754BA1F8EF318D567875139 /* warningIconSubtle@3x.png */; };
274D5DE28E0446655163A48E2272860A /* CornerRoundingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86872F4639B26435D20BED0CE7E35C7F /* CornerRoundingView.swift */; };
28D6446B971F186BACDED83CD8DE4D44 /* SwiftMessages-SwiftMessages_SwiftMessages in Resources */ = {isa = PBXBuildFile; fileRef = BEBF018059B0DFCAC8494ABD1C578AD9 /* SwiftMessages-SwiftMessages_SwiftMessages */; };
31660027252C457325491844F16AEBDD /* successIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 74793ADC8ADF7674EA6C3041E2B54354 /* successIcon.png */; };
330C59E62D17601422EDB29513B9856C /* SwiftMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC10D7DB2D227D4AA8F41CDFDDC24F37 /* SwiftMessages.swift */; };
3451B2775584DD75A3BDF9BF28A03F76 /* SwiftMessagesSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17EF282E0A0F329DDF17226E83706F8 /* SwiftMessagesSegue.swift */; };
3479628DE3B6C9005316A7F591F5E68F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; };
360BEE76BBF40319E6638B3E027E1474 /* errorIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = EDFC2E756AAB98672C4B241097AAF0F6 /* errorIconSubtle.png */; };
3613DBA9CBD7E092BD7DC1924E6648D2 /* infoIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = ACCCF5B75EA41D844ED936A92602C5B1 /* infoIconLight@2x.png */; };
37A8DD40AF13BCABE7A3CD8EF15A4C53 /* successIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D146564C0660B5F616D6736F6560E17C /* successIconSubtle@2x.png */; };
3C566158256E9CC711B73810B2C64E3B /* KeyboardTrackingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A99EE79DBFA3C24A372DDB69162E36C /* KeyboardTrackingView.swift */; };
3F53FCE80C716C7DE7A3B1D8D65077BC /* CALayer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F5FB13175EC0E13D53B445CE31395A /* CALayer+Extensions.swift */; };
422B422F962FD298F963F84934A21529 /* infoIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = C3701E97FDB85EFCE90E06D8CBC8ED41 /* infoIconLight.png */; };
436D8C63FBE86FF8271ABC32F9A52A9D /* Pods-iMessageDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 14BF989232A1D55A0FDAAB70B5A8E1BF /* Pods-iMessageDemo-dummy.m */; };
450919BA3A4D984DC24822CD80483419 /* TopBottomPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A102DE8896931D80E06F2C74C2B003AC /* TopBottomPresentable.swift */; };
45A4B95F1F73413A3001B95E1F91D697 /* UIWindow+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC808D0C98F682E01DC43B544ED093F /* UIWindow+Extensions.swift */; };
4609D5EC365EABFE507781BF911B887B /* warningIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 004EE347886E2355CCB352F2AE7A35D6 /* warningIconLight.png */; };
469995B0FC2E3748F59D0E3A4445E197 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 975A42F4C16B65B38769916EC5468D2A /* CenteredView.xib */; };
47CC2A912B82A499DB2F5E2ADBE664CF /* HapticMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA8BFA921A44ED7AEAA0E08BCE17EDDA /* HapticMessage.swift */; };
47EFB870CC797D2F54396F0AEBC8F576 /* errorIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = EC387534FD34764D4CC28B276A500AD6 /* errorIconLight.png */; };
48ADA8D4633776558E7D216434057A2A /* SwiftMessages.Config+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83D1CC131CDAC7CA2C5BFD8701E8A79 /* SwiftMessages.Config+Extensions.swift */; };
4980F404C2B391C621CE2BCF05E08BDF /* errorIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C12E87D0EA045A56FC0B45F072C74D2 /* errorIconSubtle@3x.png */; };
4C9FED5D268E82325C17309FB8904C43 /* warningIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = B8B9892D8D5596E09F02DA2D71BF60B4 /* warningIcon@3x.png */; };
52A01853AC4F2D0E720579D7F8A531CF /* WindowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9908774A62A95BF5BA8FE65842868043 /* WindowViewController.swift */; };
60F02D519964FFEE8A21221FF5E0B0C5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; };
61F532D30833A9FF1DE00EC98BC018C2 /* successIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5686DFC1E0B24A3051EF376AD8A162C0 /* successIconLight@3x.png */; };
634267873AC0F98140DBAFFF9A180BA8 /* MarginAdjustable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B602E4E64DAEF1468DD2F5C2C5DFD00 /* MarginAdjustable+Extensions.swift */; };
6583C565BE5BB9E52213854AA85F5A93 /* warningIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 6924A0A48B715EA35080ABB106B96BF7 /* warningIconSubtle.png */; };
67DC4D170143131B8AFFB558FDD00465 /* SwiftMessages-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D091A06E439F734980F26F81E9161EF2 /* SwiftMessages-dummy.m */; };
67F7D444EAC4475021B3458B21B2A162 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88398B7A70E5B00495B670516D964701 /* Identifiable.swift */; };
698F203577674AFA4BBCDE93B01B9EEC /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EB79F63EA99ADBBDA408015A4599BB /* UIViewController+Extensions.swift */; };
6A04C3E1C3437ED6BC6F096302B458AB /* errorIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8605740B16E17342A29212D21C380249 /* errorIcon@2x.png */; };
6CDE06668BB53C70BF1456F8EEE845D9 /* infoIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C40BBCF3A77C5C7A01708634BB4275C3 /* infoIconSubtle@3x.png */; };
6DC0B420F622E197919276CFF2C063CE /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790409B3FBA491A9262ACCB929F30104 /* BaseView.swift */; };
72026C4424B7A1650CBEED98ED9AA288 /* successIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 7B9CEE1B17C98E76723E7404C532515D /* successIconSubtle.png */; };
744EA4AF597AFEFEE5B398434F47CA26 /* warningIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 71699C81494BC6585B862CDFC1CDAF54 /* warningIconLight@2x.png */; };
7590FF543E2634A83059E509D5B05583 /* Pods-iMessageExtensionDemo-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CAB201AD00CAB811B045E2FFB5C03A8 /* Pods-iMessageExtensionDemo-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
79BB175F4750822409AB9ED0A9D90D09 /* successIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6EA088414C10E47808EA8490D9BBF64F /* successIcon@3x.png */; };
81B1015F29547CAB449757FC5ABCAFAB /* warningIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5FE01208AD70F1B35201AB04E50A23EC /* warningIconLight@3x.png */; };
88D024879AEAE247899BFB50C8516515 /* Pods-iMessageDemo-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0ECE831FB5E0EE1D68E837671320C7 /* Pods-iMessageDemo-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
8CB7CCDF07E1CE6838C7278BC19968B6 /* infoIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 79A086337846180B9A63D8492CCE45B4 /* infoIcon.png */; };
8F63F1ABAB21FA99A6CB1A54D5F2B5AA /* infoIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 560722B244EB84DF745199247129C164 /* infoIcon@3x.png */; };
942468E4C1AEFCF10D5043243B4007E7 /* NSBundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073CD58B2E8E255484D239A01D707B81 /* NSBundle+Extensions.swift */; };
9FBBA6E4D3A2A4CA4BA8535E35D54CA8 /* successIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 44DAC25C8D6D72F70BD1CF9099BDAF10 /* successIconLight@2x.png */; };
A4724FC3B4F49DCBF50D395257BB6B8E /* CardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03B5D12D54ACC75F929389CFF15E91FF /* CardView.xib */; };
A52191D65F4C725EBF61D27F40FEA970 /* infoIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 88237113DAAD8ECF8D5E3FAA27D8A02F /* infoIconLight@3x.png */; };
A836F848C83565B2A0A06AB23D491946 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BBC4BF82FBB152E34F2D1EF36A0FB1 /* MessageView.swift */; };
A865FCE8718103C8E725EC1EC0F9F4F6 /* BackgroundViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C1C9601BF8A5BC563062C310C0E3B5 /* BackgroundViewable.swift */; };
A8C4AD87F73E9C51F797F34BCFF5AA17 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; };
A8EF6BE26B31A8713E37E0629F6466EB /* successIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 252A5DF0FE3237691B8C50B969857389 /* successIconLight.png */; };
A90C06B459CC64A243DD1184024E5D22 /* infoIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = A6C7FF0D9E13571262231B72D0553E49 /* infoIconSubtle.png */; };
B04AA81A8A8CE0BCEFD5FAFD841F461F /* Task+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9610DCAAD9A55CDF73BAD54D523E1273 /* Task+Extensions.swift */; };
B1B4D240B65FFC5A7A981B9504C55270 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882EF814D0A1EE2AB28BFEE5C067EE6F /* NSLayoutConstraint+Extensions.swift */; };
B22AF07D79D1A82DE583AC49E3D472D3 /* Pods-iMessageExtensionDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F40CA14AD738DD186B4DA8FD14AE5BD /* Pods-iMessageExtensionDemo-dummy.m */; };
B61EBE61C31E78140BF12D32A008A43B /* StatusLine.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62332870F1E2AE3FA16EE5ADC85C104A /* StatusLine.xib */; };
B646E1CD53F14E0DEE53740799C06AC5 /* SwiftMessages-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 080E269BABA7D072D8C51E29E9AC4A91 /* SwiftMessages-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
B6D0E2FA454A946D6C9A7494648F49D9 /* MessageGeometryProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28456F0421422D16D71D14D426309B3F /* MessageGeometryProxy.swift */; };
B9FDFA90A4FB68CA5357AB70427B4DCD /* TopBottomAnimationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61DD3C66AC7ADE1E9CCFE7A4A0BEA59 /* TopBottomAnimationStyle.swift */; };
BA4779B82B35B85EFE9F595624BA3943 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16B5B2B5429B01345E4DA361DE01904D /* UIEdgeInsets+Extensions.swift */; };
BAB66573FF29F6337237A8DA0EB9E3E8 /* PassthroughWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0398CE267904AF6C2FD03B3766511ECF /* PassthroughWindow.swift */; };
BBF31491EF3411DCFA7E649645229E2A /* successIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 47A710678B44C50DA470D1BCF94B87B8 /* successIconSubtle@3x.png */; };
C2031E7EC3C60B876F2AC04253D3AFC4 /* MessageViewConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06DB4311BFB347093FC18777A503B461 /* MessageViewConvertible.swift */; };
CCBFBDB03B2444C911BADDE6A6BA08AD /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9414B7316138752779A4B9C7E5C988DB /* Error.swift */; };
D3B31A631C754CAECABFA4DC672D3D0B /* warningIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 85C6BEF274CDB3807F9BD452A26C68E6 /* warningIconSubtle@2x.png */; };
D8DFC45149C57377B5C90B3457090564 /* warningIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = D47C1471CCFFBEF214DF9F0ECBDD408B /* warningIcon.png */; };
D9519DD580DF2E744AB4C4087FA28BCD /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F67BD26788FA0C70E942A7629E41C5E8 /* Animator.swift */; };
DC6C4E22FA8DD6FA491066A738132EF1 /* infoIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8026A87EEBF1A86A282563F974C4225C /* infoIconSubtle@2x.png */; };
DD406A0C002AC1D02A6E5794B1729C22 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A865E97261799942A6F29092D3D7AE /* PassthroughView.swift */; };
DE2531A67AEBFED61A060DF13790C3BE /* errorIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 91013DDC05BDB2787740B0C9E5444D23 /* errorIcon@3x.png */; };
E03C930AD56F078DCD1357E5C83AD67A /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32459A84426DD5E88256AD61F3D4E91 /* Theme.swift */; };
E4906D67B9F27E11EE9F8E8F5C847A14 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D245E0514AAC1A2B9A6D5EA2F383E90F /* UIKit.framework */; };
E54040398A9AD751393A2171DC64D70E /* errorIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 71D489FBDD84B41576199298F717BC08 /* errorIcon.png */; };
EE9A5A3877D99BFCA5DA70BA4E049CA9 /* successIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4DBF06E9B2F93F081139B8A6A922F569 /* successIcon@2x.png */; };
F2D3ABB0C7B6EB5B531E4E68C40AA89C /* PhysicsPanHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AEEED9FBEA6C5DB7EE3B95E7D8115B3 /* PhysicsPanHandler.swift */; };
F516490B5276943A352D597431B079B6 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F1B1264A726D334A72AB1CB001CA9F /* TopBottomAnimation.swift */; };
F667CC7C7F3285FC39427F2AAA8E7665 /* PhysicsAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D7E08E0434930866B59A3F17A3A577B /* PhysicsAnimation.swift */; };
F8D0D33FC1C833490057108C322E56CB /* MaskingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4854845AC1848C056C36719A5D7420 /* MaskingView.swift */; };
FC0E2BA3CD9C6C3ACF3681841D8EF5BA /* errorIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C49B964EF7710D818E0AF0321724EEF4 /* errorIconLight@2x.png */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
146D3A65FCE17293CC3AD6CBEAE15F9A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 1FC5E8328653C350899229BDF89FACE5;
remoteInfo = "SwiftMessages-SwiftMessages_SwiftMessages";
};
70E6358901408AED062E820735630F69 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */;
proxyType = 1;
remoteGlobalIDString = DAB613A18652334F6BFC5F27BADF515D;
remoteInfo = SwiftMessages;
};
D297C84AE0F13FB6A76F2F391D21D679 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */;
proxyType = 1;
remoteGlobalIDString = DAB613A18652334F6BFC5F27BADF515D;
remoteInfo = SwiftMessages;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
004EE347886E2355CCB352F2AE7A35D6 /* warningIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = warningIconLight.png; path = SwiftMessages/Resources/warningIconLight.png; sourceTree = ""; };
0398CE267904AF6C2FD03B3766511ECF /* PassthroughWindow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PassthroughWindow.swift; path = SwiftMessages/PassthroughWindow.swift; sourceTree = ""; };
03A865E97261799942A6F29092D3D7AE /* PassthroughView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PassthroughView.swift; path = SwiftMessages/PassthroughView.swift; sourceTree = ""; };
03B5D12D54ACC75F929389CFF15E91FF /* CardView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = CardView.xib; path = SwiftMessages/Resources/CardView.xib; sourceTree = ""; };
06DB4311BFB347093FC18777A503B461 /* MessageViewConvertible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MessageViewConvertible.swift; path = SwiftMessages/MessageViewConvertible.swift; sourceTree = ""; };
073CD58B2E8E255484D239A01D707B81 /* NSBundle+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSBundle+Extensions.swift"; path = "SwiftMessages/NSBundle+Extensions.swift"; sourceTree = ""; };
080E269BABA7D072D8C51E29E9AC4A91 /* SwiftMessages-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftMessages-umbrella.h"; sourceTree = ""; };
093D5BBE2A96A1A7AC0432A3AB933576 /* Pods-iMessageDemo-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iMessageDemo-Info.plist"; sourceTree = ""; };
0FD758C8718F3EEFB5E97E34A29CE1CC /* SwiftMessages-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SwiftMessages-Info.plist"; sourceTree = ""; };
1341BB7116EC50FDF7062C6A91DEDF49 /* Pods-iMessageExtensionDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iMessageExtensionDemo-acknowledgements.plist"; sourceTree = ""; };
14BF989232A1D55A0FDAAB70B5A8E1BF /* Pods-iMessageDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iMessageDemo-dummy.m"; sourceTree = ""; };
14DF8F43552C4F1C4CAEFBAFD9A77004 /* MarginAdjustable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarginAdjustable.swift; path = SwiftMessages/MarginAdjustable.swift; sourceTree = ""; };
16B5B2B5429B01345E4DA361DE01904D /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIEdgeInsets+Extensions.swift"; path = "SwiftMessages/UIEdgeInsets+Extensions.swift"; sourceTree = ""; };
252A5DF0FE3237691B8C50B969857389 /* successIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIconLight.png; path = SwiftMessages/Resources/successIconLight.png; sourceTree = ""; };
28456F0421422D16D71D14D426309B3F /* MessageGeometryProxy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MessageGeometryProxy.swift; path = SwiftMessages/MessageGeometryProxy.swift; sourceTree = ""; };
2AB9866F195EB0AED31A33AEC27B67C1 /* WindowScene.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WindowScene.swift; path = SwiftMessages/WindowScene.swift; sourceTree = ""; };
2CF5DC74916EBE02E70234620CD80150 /* infoIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIcon@2x.png"; path = "SwiftMessages/Resources/infoIcon@2x.png"; sourceTree = ""; };
2D0ECE831FB5E0EE1D68E837671320C7 /* Pods-iMessageDemo-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iMessageDemo-umbrella.h"; sourceTree = ""; };
35F5FB13175EC0E13D53B445CE31395A /* CALayer+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CALayer+Extensions.swift"; path = "SwiftMessages/CALayer+Extensions.swift"; sourceTree = ""; };
38D28C78FCAE6706F1294CEFA6B25F88 /* errorIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconSubtle@2x.png"; path = "SwiftMessages/Resources/errorIconSubtle@2x.png"; sourceTree = ""; };
442F9FAEDF369CADB9E6E8FF3668FD95 /* Weak.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SwiftMessages/Weak.swift; sourceTree = ""; };
44DAC25C8D6D72F70BD1CF9099BDAF10 /* successIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconLight@2x.png"; path = "SwiftMessages/Resources/successIconLight@2x.png"; sourceTree = ""; };
47A710678B44C50DA470D1BCF94B87B8 /* successIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconSubtle@3x.png"; path = "SwiftMessages/Resources/successIconSubtle@3x.png"; sourceTree = ""; };
4824F23D80FF9070A5F8A452DB11EB9A /* SwiftMessages */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SwiftMessages; path = SwiftMessages.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4DBF06E9B2F93F081139B8A6A922F569 /* successIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIcon@2x.png"; path = "SwiftMessages/Resources/successIcon@2x.png"; sourceTree = ""; };
560722B244EB84DF745199247129C164 /* infoIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIcon@3x.png"; path = "SwiftMessages/Resources/infoIcon@3x.png"; sourceTree = ""; };
5686DFC1E0B24A3051EF376AD8A162C0 /* successIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconLight@3x.png"; path = "SwiftMessages/Resources/successIconLight@3x.png"; sourceTree = ""; };
573965F15F83BD70EEAC9E1E8B0F85C6 /* SwiftMessages.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = SwiftMessages.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
57EB79F63EA99ADBBDA408015A4599BB /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIViewController+Extensions.swift"; path = "SwiftMessages/UIViewController+Extensions.swift"; sourceTree = ""; };
5CAB201AD00CAB811B045E2FFB5C03A8 /* Pods-iMessageExtensionDemo-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iMessageExtensionDemo-umbrella.h"; sourceTree = ""; };
5E02B1554754BA1F8EF318D567875139 /* warningIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconSubtle@3x.png"; path = "SwiftMessages/Resources/warningIconSubtle@3x.png"; sourceTree = ""; };
5F989D33CAC3BCDA9B3C4B18F23A4DBE /* MessageHostingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MessageHostingView.swift; path = SwiftMessages/MessageHostingView.swift; sourceTree = ""; };
5FE01208AD70F1B35201AB04E50A23EC /* warningIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconLight@3x.png"; path = "SwiftMessages/Resources/warningIconLight@3x.png"; sourceTree = ""; };
6228A8E37F7905D75917DD4CBE65B03C /* SwiftMessages.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftMessages.modulemap; sourceTree = ""; };
62332870F1E2AE3FA16EE5ADC85C104A /* StatusLine.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = StatusLine.xib; path = SwiftMessages/Resources/StatusLine.xib; sourceTree = ""; };
6489B2A759075E9DC1D1406734F45B5F /* Pods-iMessageExtensionDemo.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-iMessageExtensionDemo.modulemap"; sourceTree = ""; };
6924A0A48B715EA35080ABB106B96BF7 /* warningIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = warningIconSubtle.png; path = SwiftMessages/Resources/warningIconSubtle.png; sourceTree = ""; };
6C12E87D0EA045A56FC0B45F072C74D2 /* errorIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconSubtle@3x.png"; path = "SwiftMessages/Resources/errorIconSubtle@3x.png"; sourceTree = ""; };
6EA088414C10E47808EA8490D9BBF64F /* successIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIcon@3x.png"; path = "SwiftMessages/Resources/successIcon@3x.png"; sourceTree = ""; };
71699C81494BC6585B862CDFC1CDAF54 /* warningIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconLight@2x.png"; path = "SwiftMessages/Resources/warningIconLight@2x.png"; sourceTree = ""; };
71D489FBDD84B41576199298F717BC08 /* errorIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = errorIcon.png; path = SwiftMessages/Resources/errorIcon.png; sourceTree = ""; };
74793ADC8ADF7674EA6C3041E2B54354 /* successIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIcon.png; path = SwiftMessages/Resources/successIcon.png; sourceTree = ""; };
790409B3FBA491A9262ACCB929F30104 /* BaseView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BaseView.swift; path = SwiftMessages/BaseView.swift; sourceTree = ""; };
79A086337846180B9A63D8492CCE45B4 /* infoIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = infoIcon.png; path = SwiftMessages/Resources/infoIcon.png; sourceTree = ""; };
7B9CEE1B17C98E76723E7404C532515D /* successIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIconSubtle.png; path = SwiftMessages/Resources/successIconSubtle.png; sourceTree = ""; };
7CC6A596A9C1659D8E93222DA4144414 /* Pods-iMessageExtensionDemo-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iMessageExtensionDemo-Info.plist"; sourceTree = ""; };
7F40CA14AD738DD186B4DA8FD14AE5BD /* Pods-iMessageExtensionDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iMessageExtensionDemo-dummy.m"; sourceTree = ""; };
8026A87EEBF1A86A282563F974C4225C /* infoIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconSubtle@2x.png"; path = "SwiftMessages/Resources/infoIconSubtle@2x.png"; sourceTree = ""; };
820B743874F7AC9F9E3970D68E2E60FA /* Pods-iMessageDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iMessageDemo.release.xcconfig"; sourceTree = ""; };
85C6BEF274CDB3807F9BD452A26C68E6 /* warningIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconSubtle@2x.png"; path = "SwiftMessages/Resources/warningIconSubtle@2x.png"; sourceTree = ""; };
8605740B16E17342A29212D21C380249 /* errorIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIcon@2x.png"; path = "SwiftMessages/Resources/errorIcon@2x.png"; sourceTree = ""; };
86872F4639B26435D20BED0CE7E35C7F /* CornerRoundingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CornerRoundingView.swift; path = SwiftMessages/CornerRoundingView.swift; sourceTree = ""; };
86C1C9601BF8A5BC563062C310C0E3B5 /* BackgroundViewable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BackgroundViewable.swift; path = SwiftMessages/BackgroundViewable.swift; sourceTree = ""; };
88237113DAAD8ECF8D5E3FAA27D8A02F /* infoIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconLight@3x.png"; path = "SwiftMessages/Resources/infoIconLight@3x.png"; sourceTree = ""; };
882EF814D0A1EE2AB28BFEE5C067EE6F /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSLayoutConstraint+Extensions.swift"; path = "SwiftMessages/NSLayoutConstraint+Extensions.swift"; sourceTree = ""; };
88398B7A70E5B00495B670516D964701 /* Identifiable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Identifiable.swift; path = SwiftMessages/Identifiable.swift; sourceTree = ""; };
8940290DCDAD08810FE0CD944F376444 /* ResourceBundle-SwiftMessages_SwiftMessages-SwiftMessages-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-SwiftMessages_SwiftMessages-SwiftMessages-Info.plist"; sourceTree = ""; };
8AEEED9FBEA6C5DB7EE3B95E7D8115B3 /* PhysicsPanHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PhysicsPanHandler.swift; path = SwiftMessages/PhysicsPanHandler.swift; sourceTree = ""; };
8D18688EA04EA2F2BC48ED05D1FE5404 /* Presenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Presenter.swift; path = SwiftMessages/Presenter.swift; sourceTree = ""; };
8D54691037F1CA4653B76F0558E2AA82 /* Pods-iMessageExtensionDemo-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-iMessageExtensionDemo-acknowledgements.markdown"; sourceTree = ""; };
8D7E08E0434930866B59A3F17A3A577B /* PhysicsAnimation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PhysicsAnimation.swift; path = SwiftMessages/PhysicsAnimation.swift; sourceTree = ""; };
91013DDC05BDB2787740B0C9E5444D23 /* errorIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIcon@3x.png"; path = "SwiftMessages/Resources/errorIcon@3x.png"; sourceTree = ""; };
915DE2E4E300BAD440BE13F72E49D731 /* Pods-iMessageDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iMessageDemo-acknowledgements.plist"; sourceTree = ""; };
9414B7316138752779A4B9C7E5C988DB /* Error.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Error.swift; path = SwiftMessages/Error.swift; sourceTree = ""; };
9610DCAAD9A55CDF73BAD54D523E1273 /* Task+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Task+Extensions.swift"; path = "SwiftMessages/Task+Extensions.swift"; sourceTree = ""; };
975A42F4C16B65B38769916EC5468D2A /* CenteredView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = CenteredView.xib; path = SwiftMessages/Resources/CenteredView.xib; sourceTree = ""; };
9908774A62A95BF5BA8FE65842868043 /* WindowViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WindowViewController.swift; path = SwiftMessages/WindowViewController.swift; sourceTree = ""; };
9A99EE79DBFA3C24A372DDB69162E36C /* KeyboardTrackingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KeyboardTrackingView.swift; path = SwiftMessages/KeyboardTrackingView.swift; sourceTree = ""; };
9B602E4E64DAEF1468DD2F5C2C5DFD00 /* MarginAdjustable+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "MarginAdjustable+Extensions.swift"; path = "SwiftMessages/MarginAdjustable+Extensions.swift"; sourceTree = ""; };
9D5A75A919AE6C6D38A627636D7ADC11 /* LICENSE.md */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE.md; sourceTree = ""; };
9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
A102DE8896931D80E06F2C74C2B003AC /* TopBottomPresentable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopBottomPresentable.swift; path = SwiftMessages/TopBottomPresentable.swift; sourceTree = ""; };
A1F1B1264A726D334A72AB1CB001CA9F /* TopBottomAnimation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopBottomAnimation.swift; path = SwiftMessages/TopBottomAnimation.swift; sourceTree = ""; };
A399825CF45C4427BF9B80A590BF2BD5 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; };
A4D741EF8B40F5472CE2C1C1A34426BD /* MessageView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = MessageView.xib; path = SwiftMessages/Resources/MessageView.xib; sourceTree = ""; };
A6C7FF0D9E13571262231B72D0553E49 /* infoIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = infoIconSubtle.png; path = SwiftMessages/Resources/infoIconSubtle.png; sourceTree = ""; };
ACCCF5B75EA41D844ED936A92602C5B1 /* infoIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconLight@2x.png"; path = "SwiftMessages/Resources/infoIconLight@2x.png"; sourceTree = ""; };
AE7AEA9CE6B44DCC96AE4E68FA644DAA /* Pods-iMessageExtensionDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iMessageExtensionDemo.release.xcconfig"; sourceTree = ""; };
AFC41396FB1BD59C9A69EE1DD82E47C2 /* Pods-iMessageDemo */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-iMessageDemo"; path = Pods_iMessageDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B17EF282E0A0F329DDF17226E83706F8 /* SwiftMessagesSegue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftMessagesSegue.swift; path = SwiftMessages/SwiftMessagesSegue.swift; sourceTree = ""; };
B19DACBEC8DD85FB850D299DFB7A0374 /* SwiftMessageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftMessageModifier.swift; path = SwiftMessages/SwiftMessageModifier.swift; sourceTree = ""; };
B241F1B5D295E7806363B802DF03A085 /* warningIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIcon@2x.png"; path = "SwiftMessages/Resources/warningIcon@2x.png"; sourceTree = ""; };
B32459A84426DD5E88256AD61F3D4E91 /* Theme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = SwiftMessages/Theme.swift; sourceTree = ""; };
B8B9892D8D5596E09F02DA2D71BF60B4 /* warningIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIcon@3x.png"; path = "SwiftMessages/Resources/warningIcon@3x.png"; sourceTree = ""; };
B941E5186F814BF618D9D0458496379C /* SwiftMessages-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftMessages-prefix.pch"; sourceTree = "