diff --git a/.gitignore b/.gitignore index 1c055573..3c698b88 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ playground.xcworkspace # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ -.build/ +.swiftpm/ .DS_Store *.mobileprovision @@ -47,3 +47,4 @@ playground.xcworkspace *.pkey screenshots rvm.env +.build \ No newline at end of file diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 9f55b2cc..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -3.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ea342f00..1fa85286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,440 @@ # Change Log All notable changes to this project will be documented in this file. +## 10.0.1 + +## 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 diff --git a/Demo/Podfile b/Demo/.Podfile similarity index 51% rename from Demo/Podfile rename to Demo/.Podfile index e73d6059..e1c67ab0 100644 --- a/Demo/Podfile +++ b/Demo/.Podfile @@ -2,5 +2,7 @@ target 'Demo' do use_frameworks! workspace 'Demo.xcworkspace' xcodeproj 'Demo.xcodeproj' - pod 'SwiftMessages', :path => '../' + pod 'SwiftMessages/App', :path => '../' + pod 'SwiftMessages/SegueExtras', :path => '../' end + diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 7507ff4d..57a9b7a5 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,21 +7,84 @@ objects = { /* Begin PBXBuildFile section */ - 49BBBE829403C46C88EE748E /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 094A4CD596AFE2FDC36B2833 /* Pods_Demo.framework */; }; + 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 */; }; - 86C0ABA01D5E814600F76BD6 /* TacoDialogView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86C0AB9F1D5E814600F76BD6 /* TacoDialogView.xib */; }; 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 */ - 094A4CD596AFE2FDC36B2833 /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 160AC8A3EE68E1D705664BF8 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; - 16131DA1E3BA049C9E4C0308 /* Pods-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.release.xcconfig"; path = "Pods/Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig"; sourceTree = ""; }; + 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 = ""; }; @@ -39,28 +102,28 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 49BBBE829403C46C88EE748E /* Pods_Demo.framework in Frameworks */, + 22FE3FA821193CB90017303D /* SwiftMessages.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 6AB98897E817EE976DD72852 /* Frameworks */ = { + 22FB323B21193A3B005C13D9 /* Products */ = { isa = PBXGroup; children = ( - 094A4CD596AFE2FDC36B2833 /* Pods_Demo.framework */, + 22FB324121193A3B005C13D9 /* SwiftMessages.framework */, + 22FB324521193A3B005C13D9 /* SwiftMessagesTests.xctest */, ); - name = Frameworks; + name = Products; sourceTree = ""; }; - 7AEB2A3BA8EF9DC4D87CD5FA /* Pods */ = { + 6AB98897E817EE976DD72852 /* Frameworks */ = { isa = PBXGroup; children = ( - 160AC8A3EE68E1D705664BF8 /* Pods-Demo.debug.xcconfig */, - 16131DA1E3BA049C9E4C0308 /* Pods-Demo.release.xcconfig */, + 22774C1420B8461900813732 /* Messages.framework */, ); - name = Pods; + name = Frameworks; sourceTree = ""; }; 86AEDCD91D5D1DB70030232E = { @@ -68,8 +131,8 @@ children = ( 86AEDCE41D5D1DB70030232E /* Demo */, 86AEDCE31D5D1DB70030232E /* Products */, - 7AEB2A3BA8EF9DC4D87CD5FA /* Pods */, 6AB98897E817EE976DD72852 /* Frameworks */, + 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */, ); sourceTree = ""; }; @@ -85,14 +148,18 @@ isa = PBXGroup; children = ( 86AEDCE51D5D1DB70030232E /* AppDelegate.swift */, + 86AEDCE91D5D1DB70030232E /* Main.storyboard */, 86AEDCE71D5D1DB70030232E /* ViewController.swift */, 8642F4351D5F7F540061BDCD /* ExploreViewController.swift */, - 86AEDCE91D5D1DB70030232E /* Main.storyboard */, + 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 = ""; @@ -104,16 +171,17 @@ isa = PBXNativeTarget; buildConfigurationList = 86AEDCF41D5D1DB70030232E /* Build configuration list for PBXNativeTarget "Demo" */; buildPhases = ( - D459C3DF988BEB6ED9625B99 /* [CP] Check Pods Manifest.lock */, 86AEDCDE1D5D1DB70030232E /* Sources */, 86AEDCDF1D5D1DB70030232E /* Frameworks */, 86AEDCE01D5D1DB70030232E /* Resources */, - 1EB2C90557DD78EF0B2FD638 /* [CP] Embed Pods Frameworks */, - 2FFDC4A45AD78DB76BCCD90F /* [CP] Copy Pods Resources */, + 22774C2520B8461A00813732 /* Embed App Extensions */, + 22FE3FB021193CB90017303D /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 22FB324721193A4D005C13D9 /* PBXTargetDependency */, + 22FE3FAB21193CB90017303D /* PBXTargetDependency */, ); name = Demo; productName = Demo; @@ -126,19 +194,19 @@ 86AEDCDA1D5D1DB70030232E /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0810; + LastSwiftUpdateCheck = 0940; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "SwiftKick Mobile"; TargetAttributes = { 86AEDCE11D5D1DB70030232E = { CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 1020; }; }; }; buildConfigurationList = 86AEDCDD1D5D1DB70030232E /* Build configuration list for PBXProject "Demo" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -147,6 +215,12 @@ mainGroup = 86AEDCD91D5D1DB70030232E; productRefGroup = 86AEDCE31D5D1DB70030232E /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 22FB323B21193A3B005C13D9 /* Products */; + ProjectRef = 22FB323A21193A3B005C13D9 /* SwiftMessages.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 86AEDCE11D5D1DB70030232E /* Demo */, @@ -154,82 +228,68 @@ }; /* 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 */, - 86C0ABA01D5E814600F76BD6 /* TacoDialogView.xib in Resources */, 86AEDCEB1D5D1DB70030232E /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 1EB2C90557DD78EF0B2FD638 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 2FFDC4A45AD78DB76BCCD90F /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Demo/Pods-Demo-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - D459C3DF988BEB6ED9625B99 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase 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; @@ -254,19 +314,29 @@ 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; @@ -289,7 +359,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -302,19 +372,29 @@ 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; @@ -331,7 +411,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -342,29 +422,35 @@ }; 86AEDCF51D5D1DB70030232E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 160AC8A3EE68E1D705664BF8 /* Pods-Demo.debug.xcconfig */; 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.Demo; + PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages.Demo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 86AEDCF61D5D1DB70030232E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 16131DA1E3BA049C9E4C0308 /* Pods-Demo.release.xcconfig */; 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.Demo; + PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages.Demo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme new file mode 100644 index 00000000..db82f1fa --- /dev/null +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Demo/AppDelegate.swift b/Demo/Demo/AppDelegate.swift index a3ca313a..ab254933 100644 --- a/Demo/Demo/AppDelegate.swift +++ b/Demo/Demo/AppDelegate.swift @@ -8,37 +8,17 @@ 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: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + window?.tintColor = brandColor + UISwitch.appearance().onTintColor = brandColor return true } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - } diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json index b8236c65..2eeb86b6 100644 --- a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,44 +1,118 @@ { "images" : [ { - "idiom" : "iphone", "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "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" }, { - "idiom" : "iphone", "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { - "idiom" : "iphone", "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { - "idiom" : "iphone", "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { - "idiom" : "iphone", "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" : { diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..f3637f34 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png new file mode 100644 index 00000000..7cad51b4 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png new file mode 100644 index 00000000..7cad51b4 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..7cad51b4 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..76c56454 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png new file mode 100644 index 00000000..0849f153 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..0849f153 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png new file mode 100644 index 00000000..48315c2e Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..48315c2e Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..b26f4bc9 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png new file mode 100644 index 00000000..3ae27f01 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..3ae27f01 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..ce7c60fa Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..ce7c60fa Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..006d9164 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..1560730f Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..a7187cb7 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..4482fe43 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png new file mode 100644 index 00000000..c6c98fdf Binary files /dev/null and b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png differ diff --git a/Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf b/Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf index 19fbe440..47dca35c 100644 Binary files a/Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf and b/Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf differ diff --git a/SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/Contents.json b/Demo/Demo/Assets.xcassets/splashBanner.imageset/Contents.json similarity index 63% rename from SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/Contents.json rename to Demo/Demo/Assets.xcassets/splashBanner.imageset/Contents.json index 211a2ee0..7195c55c 100644 --- a/SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/Contents.json +++ b/Demo/Demo/Assets.xcassets/splashBanner.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "infoIcon.pdf" + "filename" : "splashBanner.pdf" } ], "info" : { @@ -10,6 +10,6 @@ "author" : "xcode" }, "properties" : { - "template-rendering-intent" : "template" + "template-rendering-intent" : "original" } } \ No newline at end of file diff --git a/Demo/Demo/Assets.xcassets/splashBanner.imageset/splashBanner.pdf b/Demo/Demo/Assets.xcassets/splashBanner.imageset/splashBanner.pdf new file mode 100644 index 00000000..0ee52801 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/splashBanner.imageset/splashBanner.pdf differ diff --git a/Demo/Demo/Base.lproj/LaunchScreen.storyboard b/Demo/Demo/Base.lproj/LaunchScreen.storyboard index 12e77ef2..4fb64dba 100644 --- a/Demo/Demo/Base.lproj/LaunchScreen.storyboard +++ b/Demo/Demo/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,9 @@ - - + + + - - + @@ -19,31 +19,24 @@ - - - + - - - - + + @@ -52,4 +45,7 @@ + + + diff --git a/Demo/Demo/Base.lproj/Main.storyboard b/Demo/Demo/Base.lproj/Main.storyboard index 2be9921a..c38d6707 100644 --- a/Demo/Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Base.lproj/Main.storyboard @@ -1,9 +1,10 @@ - + + - - + + @@ -29,25 +30,26 @@ - + - + - - + + - + @@ -66,21 +68,22 @@ - - + + - + @@ -100,6 +103,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -116,29 +222,32 @@ - - + + - + - - + + - + - + + + @@ -146,25 +255,28 @@ + - + - - + + - + - + + @@ -175,26 +287,28 @@ - + + - - + + - + - - - + + - + - + - + - + - + - + + + - + + + @@ -271,26 +393,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + + + + + - @@ -303,19 +461,20 @@ - - + + - + - + + @@ -326,27 +485,29 @@ + - + - - + + - + - - - + + - + - + + @@ -388,86 +551,88 @@ + - + - + - + - + + + - + + - + - - - - - - - + + - + - + + - + + - - + + - + - + + @@ -477,43 +642,54 @@ - + + - - + + - + + + + - + + + + - + + + @@ -521,23 +697,32 @@ + + - + + + + - + + + @@ -550,6 +735,7 @@ + @@ -568,12 +754,17 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -655,7 +1119,17 @@ + + + + - + + + + + + + diff --git a/Demo/Demo/CountedMessageView.swift b/Demo/Demo/CountedMessageView.swift new file mode 100644 index 00000000..e0921bde --- /dev/null +++ b/Demo/Demo/CountedMessageView.swift @@ -0,0 +1,19 @@ +// +// 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" + } +} diff --git a/Demo/Demo/CountedViewController.swift b/Demo/Demo/CountedViewController.swift new file mode 100644 index 00000000..6bcc9bf0 --- /dev/null +++ b/Demo/Demo/CountedViewController.swift @@ -0,0 +1,45 @@ +// +// 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() + } +} diff --git a/Demo/Demo/ExploreViewController.swift b/Demo/Demo/ExploreViewController.swift index aec24623..5d34762a 100644 --- a/Demo/Demo/ExploreViewController.swift +++ b/Demo/Demo/ExploreViewController.swift @@ -18,11 +18,11 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { let view: MessageView switch layout.selectedSegmentIndex { case 1: - view = MessageView.viewFromNib(layout: .CardView) + view = MessageView.viewFromNib(layout: .cardView) case 2: - view = MessageView.viewFromNib(layout: .TabView) + view = MessageView.viewFromNib(layout: .tabView) case 3: - view = MessageView.viewFromNib(layout: .StatusLine) + view = MessageView.viewFromNib(layout: .statusLine) default: view = try! SwiftMessages.viewFromNib() } @@ -41,17 +41,21 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { switch theme.selectedSegmentIndex { case 0: - view.configureTheme(.info, iconStyle: iconStyle) + view.configureTheme(.info, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) + view.accessibilityPrefix = "info" case 1: - view.configureTheme(.success, iconStyle: iconStyle) + view.configureTheme(.success, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) + view.accessibilityPrefix = "success" case 2: - view.configureTheme(.warning, iconStyle: iconStyle) + view.configureTheme(.warning, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) + view.accessibilityPrefix = "warning" case 3: - view.configureTheme(.error, iconStyle: iconStyle) + view.configureTheme(.error, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) + view.accessibilityPrefix = "error" default: - let iconText = ["🐸", "🐷", "🐬", "🐠", "🐍", "🐹", "🐼"].sm_random() + 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?.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) @@ -85,15 +89,17 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { switch presentationStyle.selectedSegmentIndex { case 1: config.presentationStyle = .bottom + case 2: + config.presentationStyle = .center default: break } switch presentationContext.selectedSegmentIndex { case 1: - config.presentationContext = .window(windowLevel: UIWindowLevelNormal) + config.presentationContext = .window(windowLevel: UIWindow.Level.normal) case 2: - config.presentationContext = .window(windowLevel: UIWindowLevelStatusBar) + config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar) default: break } @@ -111,9 +117,11 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { switch dimMode.selectedSegmentIndex { case 1: - config.dimMode = .gray(interactive: false) - case 2: 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 } @@ -132,7 +140,11 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { break } } - + + if view.defaultHaptic == nil && hapticFeedback.isOn { + config.haptic = .success + } + // Show SwiftMessages.show(config: config, view: view) } @@ -146,6 +158,7 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate { @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! diff --git a/Demo/Demo/Info.plist b/Demo/Demo/Info.plist index cf2e887f..1aceb410 100644 --- a/Demo/Demo/Info.plist +++ b/Demo/Demo/Info.plist @@ -35,6 +35,7 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown diff --git a/Demo/Demo/TacoDialogView.swift b/Demo/Demo/TacoDialogView.swift index a42f6df7..58739c93 100644 --- a/Demo/Demo/TacoDialogView.swift +++ b/Demo/Demo/TacoDialogView.swift @@ -24,7 +24,7 @@ class TacoDialogView: MessageView { fileprivate var count = 1 { didSet { iconLabel?.text = String(repeating: "🌮", count: count)//String(count: count, repeatedValue: ) - titleLabel?.text = TacoDialogView.tacoTitles[count] ?? "\(count)" + String(repeating: "!", count: count) + bodyLabel?.text = TacoDialogView.tacoTitles[count] ?? "\(count)" + String(repeating: "!", count: count) } } diff --git a/Demo/Demo/TacoDialogView.xib b/Demo/Demo/TacoDialogView.xib index 8dcda1a2..af7e8d3b 100644 --- a/Demo/Demo/TacoDialogView.xib +++ b/Demo/Demo/TacoDialogView.xib @@ -1,8 +1,11 @@ - - + + + + + - + @@ -13,17 +16,26 @@ - + + + + + + + @@ -73,10 +95,13 @@ + + + - - - - + + + + + - - + + + + + + + + + + + + - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - + diff --git a/SwiftMessages/Resources/CenteredView.xib b/SwiftMessages/Resources/CenteredView.xib new file mode 100644 index 00000000..a9fda2be --- /dev/null +++ b/SwiftMessages/Resources/CenteredView.xib @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftMessages/Resources/Images.xcassets/Contents.json b/SwiftMessages/Resources/Images.xcassets/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/SwiftMessages/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/Contents.json deleted file mode 100644 index fa61713f..00000000 --- a/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "errorIcon.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/errorIcon.pdf b/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/errorIcon.pdf deleted file mode 100644 index 186638c2..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/errorIcon.imageset/errorIcon.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/Contents.json deleted file mode 100644 index 2f501a23..00000000 --- a/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "errorIconLight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/errorIconLight.pdf b/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/errorIconLight.pdf deleted file mode 100644 index 27cb6b8c..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/errorIconLight.imageset/errorIconLight.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/Contents.json deleted file mode 100644 index e6b30b25..00000000 --- a/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "errorIconSubtle.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/errorIconSubtle.pdf b/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/errorIconSubtle.pdf deleted file mode 100644 index a0ab3628..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/errorIconSubtle.imageset/errorIconSubtle.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/infoIcon.pdf b/SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/infoIcon.pdf deleted file mode 100644 index 120bd67a..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/infoIcon.imageset/infoIcon.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/Contents.json deleted file mode 100644 index 1b87f941..00000000 --- a/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "infoIconLight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/infoIconLight.pdf b/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/infoIconLight.pdf deleted file mode 100644 index afab1871..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/infoIconLight.imageset/infoIconLight.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/Contents.json deleted file mode 100644 index 6e3f345b..00000000 --- a/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "infoIconSubtle.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/infoIconSubtle.pdf b/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/infoIconSubtle.pdf deleted file mode 100644 index 9aa3443d..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/infoIconSubtle.imageset/infoIconSubtle.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/Contents.json deleted file mode 100644 index 6c462cde..00000000 --- a/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "successIcon.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/successIcon.pdf b/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/successIcon.pdf deleted file mode 100644 index 2141b427..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/successIcon.imageset/successIcon.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/Contents.json deleted file mode 100644 index 2119691e..00000000 --- a/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "successIconLight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/successIconLight.pdf b/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/successIconLight.pdf deleted file mode 100644 index 91293032..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/successIconLight.imageset/successIconLight.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/Contents.json deleted file mode 100644 index 82a84d16..00000000 --- a/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "successIconSubtle.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/successIconSubtle.pdf b/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/successIconSubtle.pdf deleted file mode 100644 index 86052549..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/successIconSubtle.imageset/successIconSubtle.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/Contents.json deleted file mode 100644 index 602136f4..00000000 --- a/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "warningIcon.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/warningIcon.pdf b/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/warningIcon.pdf deleted file mode 100644 index 570e8bf6..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/warningIcon.imageset/warningIcon.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/Contents.json deleted file mode 100644 index a74cbdf3..00000000 --- a/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "warningIconLight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/warningIconLight.pdf b/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/warningIconLight.pdf deleted file mode 100644 index 32310ae5..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/warningIconLight.imageset/warningIconLight.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/Contents.json b/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/Contents.json deleted file mode 100644 index d3f2dc56..00000000 --- a/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "warningIconSubtle.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/warningIconSubtle.pdf b/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/warningIconSubtle.pdf deleted file mode 100644 index 0ad367ec..00000000 Binary files a/SwiftMessages/Resources/Images.xcassets/warningIconSubtle.imageset/warningIconSubtle.pdf and /dev/null differ diff --git a/SwiftMessages/Resources/MessageView.xib b/SwiftMessages/Resources/MessageView.xib index 21a37c26..52ceaa7d 100644 --- a/SwiftMessages/Resources/MessageView.xib +++ b/SwiftMessages/Resources/MessageView.xib @@ -1,44 +1,50 @@ - - - - + + - - - + + - + - + - + - + - - + - - - - + + + + - - + + + + + + + + + + + + @@ -78,7 +95,7 @@ - + diff --git a/SwiftMessages/Resources/MessageViewIOS8.xib b/SwiftMessages/Resources/MessageViewIOS8.xib deleted file mode 100644 index 2df350a2..00000000 --- a/SwiftMessages/Resources/MessageViewIOS8.xib +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/SwiftMessages/Resources/StatusLine.xib b/SwiftMessages/Resources/StatusLine.xib index 8485fbd0..3dbef99f 100644 --- a/SwiftMessages/Resources/StatusLine.xib +++ b/SwiftMessages/Resources/StatusLine.xib @@ -1,19 +1,23 @@ - - + + + - - + - + - + - + - - + + + + + + + + + + + + - + diff --git a/SwiftMessages/Resources/TabView.xib b/SwiftMessages/Resources/TabView.xib index b3c59b68..141467c1 100644 --- a/SwiftMessages/Resources/TabView.xib +++ b/SwiftMessages/Resources/TabView.xib @@ -1,12 +1,9 @@ - - - - + + - - - + + @@ -16,33 +13,42 @@ - - + + - + - + - - + + - - + - - - - + + + - - + + + + + + + + + + + + + - + + + - + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftMessages/Resources/errorIcon.png b/SwiftMessages/Resources/errorIcon.png new file mode 100644 index 00000000..92f59e79 Binary files /dev/null and b/SwiftMessages/Resources/errorIcon.png differ diff --git a/SwiftMessages/Resources/errorIcon@2x.png b/SwiftMessages/Resources/errorIcon@2x.png new file mode 100644 index 00000000..2a0be86e Binary files /dev/null and b/SwiftMessages/Resources/errorIcon@2x.png differ diff --git a/SwiftMessages/Resources/errorIcon@3x.png b/SwiftMessages/Resources/errorIcon@3x.png new file mode 100644 index 00000000..07a9e775 Binary files /dev/null and b/SwiftMessages/Resources/errorIcon@3x.png differ diff --git a/SwiftMessages/Resources/errorIconLight.png b/SwiftMessages/Resources/errorIconLight.png new file mode 100644 index 00000000..bd00a52f Binary files /dev/null and b/SwiftMessages/Resources/errorIconLight.png differ diff --git a/SwiftMessages/Resources/errorIconLight@2x.png b/SwiftMessages/Resources/errorIconLight@2x.png new file mode 100644 index 00000000..f4a5c669 Binary files /dev/null and b/SwiftMessages/Resources/errorIconLight@2x.png differ diff --git a/SwiftMessages/Resources/errorIconLight@3x.png b/SwiftMessages/Resources/errorIconLight@3x.png new file mode 100644 index 00000000..6360adbd Binary files /dev/null and b/SwiftMessages/Resources/errorIconLight@3x.png differ diff --git a/SwiftMessages/Resources/errorIconSubtle.png b/SwiftMessages/Resources/errorIconSubtle.png new file mode 100644 index 00000000..833f7c71 Binary files /dev/null and b/SwiftMessages/Resources/errorIconSubtle.png differ diff --git a/SwiftMessages/Resources/errorIconSubtle@2x.png b/SwiftMessages/Resources/errorIconSubtle@2x.png new file mode 100644 index 00000000..0a6d0d9d Binary files /dev/null and b/SwiftMessages/Resources/errorIconSubtle@2x.png differ diff --git a/SwiftMessages/Resources/errorIconSubtle@3x.png b/SwiftMessages/Resources/errorIconSubtle@3x.png new file mode 100644 index 00000000..d15795bb Binary files /dev/null and b/SwiftMessages/Resources/errorIconSubtle@3x.png differ diff --git a/SwiftMessages/Resources/infoIcon.png b/SwiftMessages/Resources/infoIcon.png new file mode 100644 index 00000000..16a0cc7f Binary files /dev/null and b/SwiftMessages/Resources/infoIcon.png differ diff --git a/SwiftMessages/Resources/infoIcon@2x.png b/SwiftMessages/Resources/infoIcon@2x.png new file mode 100644 index 00000000..a1ab6761 Binary files /dev/null and b/SwiftMessages/Resources/infoIcon@2x.png differ diff --git a/SwiftMessages/Resources/infoIcon@3x.png b/SwiftMessages/Resources/infoIcon@3x.png new file mode 100644 index 00000000..fb98813f Binary files /dev/null and b/SwiftMessages/Resources/infoIcon@3x.png differ diff --git a/SwiftMessages/Resources/infoIconLight.png b/SwiftMessages/Resources/infoIconLight.png new file mode 100644 index 00000000..9195a166 Binary files /dev/null and b/SwiftMessages/Resources/infoIconLight.png differ diff --git a/SwiftMessages/Resources/infoIconLight@2x.png b/SwiftMessages/Resources/infoIconLight@2x.png new file mode 100644 index 00000000..4662af3d Binary files /dev/null and b/SwiftMessages/Resources/infoIconLight@2x.png differ diff --git a/SwiftMessages/Resources/infoIconLight@3x.png b/SwiftMessages/Resources/infoIconLight@3x.png new file mode 100644 index 00000000..999376e2 Binary files /dev/null and b/SwiftMessages/Resources/infoIconLight@3x.png differ diff --git a/SwiftMessages/Resources/infoIconSubtle.png b/SwiftMessages/Resources/infoIconSubtle.png new file mode 100644 index 00000000..aa1a119e Binary files /dev/null and b/SwiftMessages/Resources/infoIconSubtle.png differ diff --git a/SwiftMessages/Resources/infoIconSubtle@2x.png b/SwiftMessages/Resources/infoIconSubtle@2x.png new file mode 100644 index 00000000..901635d4 Binary files /dev/null and b/SwiftMessages/Resources/infoIconSubtle@2x.png differ diff --git a/SwiftMessages/Resources/infoIconSubtle@3x.png b/SwiftMessages/Resources/infoIconSubtle@3x.png new file mode 100644 index 00000000..769fd02e Binary files /dev/null and b/SwiftMessages/Resources/infoIconSubtle@3x.png differ diff --git a/SwiftMessages/Resources/successIcon.png b/SwiftMessages/Resources/successIcon.png new file mode 100644 index 00000000..1d371c7f Binary files /dev/null and b/SwiftMessages/Resources/successIcon.png differ diff --git a/SwiftMessages/Resources/successIcon@2x.png b/SwiftMessages/Resources/successIcon@2x.png new file mode 100644 index 00000000..719218cf Binary files /dev/null and b/SwiftMessages/Resources/successIcon@2x.png differ diff --git a/SwiftMessages/Resources/successIcon@3x.png b/SwiftMessages/Resources/successIcon@3x.png new file mode 100644 index 00000000..74893cab Binary files /dev/null and b/SwiftMessages/Resources/successIcon@3x.png differ diff --git a/SwiftMessages/Resources/successIconLight.png b/SwiftMessages/Resources/successIconLight.png new file mode 100644 index 00000000..0e5314c6 Binary files /dev/null and b/SwiftMessages/Resources/successIconLight.png differ diff --git a/SwiftMessages/Resources/successIconLight@2x.png b/SwiftMessages/Resources/successIconLight@2x.png new file mode 100644 index 00000000..149ab077 Binary files /dev/null and b/SwiftMessages/Resources/successIconLight@2x.png differ diff --git a/SwiftMessages/Resources/successIconLight@3x.png b/SwiftMessages/Resources/successIconLight@3x.png new file mode 100644 index 00000000..5e152999 Binary files /dev/null and b/SwiftMessages/Resources/successIconLight@3x.png differ diff --git a/SwiftMessages/Resources/successIconSubtle.png b/SwiftMessages/Resources/successIconSubtle.png new file mode 100644 index 00000000..61334838 Binary files /dev/null and b/SwiftMessages/Resources/successIconSubtle.png differ diff --git a/SwiftMessages/Resources/successIconSubtle@2x.png b/SwiftMessages/Resources/successIconSubtle@2x.png new file mode 100644 index 00000000..fbe6053f Binary files /dev/null and b/SwiftMessages/Resources/successIconSubtle@2x.png differ diff --git a/SwiftMessages/Resources/successIconSubtle@3x.png b/SwiftMessages/Resources/successIconSubtle@3x.png new file mode 100644 index 00000000..6ce53797 Binary files /dev/null and b/SwiftMessages/Resources/successIconSubtle@3x.png differ diff --git a/SwiftMessages/Resources/warningIcon.png b/SwiftMessages/Resources/warningIcon.png new file mode 100644 index 00000000..77357618 Binary files /dev/null and b/SwiftMessages/Resources/warningIcon.png differ diff --git a/SwiftMessages/Resources/warningIcon@2x.png b/SwiftMessages/Resources/warningIcon@2x.png new file mode 100644 index 00000000..448d6865 Binary files /dev/null and b/SwiftMessages/Resources/warningIcon@2x.png differ diff --git a/SwiftMessages/Resources/warningIcon@3x.png b/SwiftMessages/Resources/warningIcon@3x.png new file mode 100644 index 00000000..e2c8d46f Binary files /dev/null and b/SwiftMessages/Resources/warningIcon@3x.png differ diff --git a/SwiftMessages/Resources/warningIconLight.png b/SwiftMessages/Resources/warningIconLight.png new file mode 100644 index 00000000..fce1a0e5 Binary files /dev/null and b/SwiftMessages/Resources/warningIconLight.png differ diff --git a/SwiftMessages/Resources/warningIconLight@2x.png b/SwiftMessages/Resources/warningIconLight@2x.png new file mode 100644 index 00000000..91de0d9b Binary files /dev/null and b/SwiftMessages/Resources/warningIconLight@2x.png differ diff --git a/SwiftMessages/Resources/warningIconLight@3x.png b/SwiftMessages/Resources/warningIconLight@3x.png new file mode 100644 index 00000000..11d68596 Binary files /dev/null and b/SwiftMessages/Resources/warningIconLight@3x.png differ diff --git a/SwiftMessages/Resources/warningIconSubtle.png b/SwiftMessages/Resources/warningIconSubtle.png new file mode 100644 index 00000000..e54dcd35 Binary files /dev/null and b/SwiftMessages/Resources/warningIconSubtle.png differ diff --git a/SwiftMessages/Resources/warningIconSubtle@2x.png b/SwiftMessages/Resources/warningIconSubtle@2x.png new file mode 100644 index 00000000..cfd2ebd4 Binary files /dev/null and b/SwiftMessages/Resources/warningIconSubtle@2x.png differ diff --git a/SwiftMessages/Resources/warningIconSubtle@3x.png b/SwiftMessages/Resources/warningIconSubtle@3x.png new file mode 100644 index 00000000..95715670 Binary files /dev/null and b/SwiftMessages/Resources/warningIconSubtle@3x.png differ diff --git a/SwiftMessages/SwiftMessageModifier.swift b/SwiftMessages/SwiftMessageModifier.swift new file mode 100644 index 00000000..e44d6871 --- /dev/null +++ b/SwiftMessages/SwiftMessageModifier.swift @@ -0,0 +1,133 @@ +// +// 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() + } + } + } +} diff --git a/SwiftMessages/SwiftMessages.Config+Extensions.swift b/SwiftMessages/SwiftMessages.Config+Extensions.swift new file mode 100644 index 00000000..a17a9f22 --- /dev/null +++ b/SwiftMessages/SwiftMessages.Config+Extensions.swift @@ -0,0 +1,44 @@ +// +// 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 + } + } +} diff --git a/SwiftMessages/SwiftMessages.swift b/SwiftMessages/SwiftMessages.swift index d4b4ae58..f05ad494 100644 --- a/SwiftMessages/SwiftMessages.swift +++ b/SwiftMessages/SwiftMessages.swift @@ -13,9 +13,10 @@ 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 - implement the `Identifiable` protocol (as `MessageView` does) will have duplicates removed. + adopt the `Identifiable` protocol (as `MessageView` does) will have duplicates removed. */ -open class SwiftMessages: PresenterDelegate { +@MainActor +open class SwiftMessages { /** Specifies whether the message view is displayed at the top or bottom @@ -32,6 +33,16 @@ open class SwiftMessages: PresenterDelegate { Message view slides up from the bottom. */ case bottom + + /** + Message view fades into the center. + */ + case center + + /** + User-defined animation + */ + case custom(animator: Animator) } /** @@ -43,32 +54,44 @@ open class SwiftMessages: PresenterDelegate { /** 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 `UIWindowLevelNormal`. Use this option to automatically display + at level `UIWindow.Level.normal`. Use this option to automatically display under bars, where applicable. Because this option involves a top-down - search, an approrpiate context might not be found when the view controller - heirarchy incorporates custom containers. If this is the case, the + 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. Use - `UIWindowLevelNormal` to display under the status bar and `UIWindowLevelStatusBar` - to display over. When displaying under the status bar, SwiftMessages automatically - increases the top margins of any message view that adopts the `MarginInsetting` - protocol (as `MessageView` does) to account for the status bar. + 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: UIWindowLevel) - + 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 heirarchy. + for targeted placement in a view controller hierarchy. */ case viewController(_: UIViewController) - + /** Displays the message view in the given container view. */ @@ -97,6 +120,49 @@ open class SwiftMessages: PresenterDelegate { - 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 } /** @@ -113,8 +179,8 @@ open class SwiftMessages: PresenterDelegate { /** Dim the background behind the message view a gray color. - - Parameter interactive: Specifies whether or not tapping the - dimmed area dismisses the message view. + - `interactive`: Specifies whether or not tapping the + dimmed area dismisses the message view. */ case gray(interactive: Bool) @@ -123,21 +189,67 @@ open class SwiftMessages: PresenterDelegate { SwiftMessages does not apply alpha transparency to the color, so any alpha must be baked into the `UIColor` instance. - - Parameter color: The color of the dim view. - - Parameter interactive: Specifies whether or not tapping the - dimmed area dismisses the message view. + - `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 - case didShow - case willHide - case didHide + 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 + } } /** @@ -177,6 +289,13 @@ open class SwiftMessages: PresenterDelegate { */ 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` @@ -188,15 +307,22 @@ open class SwiftMessages: PresenterDelegate { public var interactiveHide = true /** - Specifies the preferred status bar style when the view is displayed - directly behind the status bar, such as when using `.Window` - presentation context with a `UIWindowLevelNormal` window level - and `.Top` presentation style. This option is useful if the message - view has a background color that needs a different status bar style than - the current one. The default is `.Default`. + 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 @@ -229,14 +355,56 @@ open class SwiftMessages: PresenterDelegate { > 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 = false + 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. */ - public init() {} - + nonisolated public init() {} + /** Adds the given configuration and view to the message queue to be displayed. @@ -244,14 +412,8 @@ open class SwiftMessages: PresenterDelegate { - Parameter view: The view to be displayed. */ open func show(config: Config, view: UIView) { - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } - let presenter = Presenter(config: config, view: view, delegate: strongSelf) - strongSelf.syncQueue.async { [weak self] in - guard let strongSelf = self else { return } - strongSelf.enqueue(presenter: presenter) - } - } + let presenter = Presenter(config: config, view: view, delegate: self) + enqueue(presenter: presenter) } /** @@ -278,11 +440,11 @@ open class SwiftMessages: PresenterDelegate { - Parameter config: The configuration options. - Parameter viewProvider: A block that returns the view to be displayed. */ - open func show(config: Config, viewProvider: @escaping ViewProvider) { - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } + nonisolated open func show(config: Config, viewProvider: @escaping ViewProvider) { + Task { @MainActor [weak self] in + guard let self else { return } let view = viewProvider() - strongSelf.show(config: config, view: view) + self.show(config: config, view: view) } } @@ -303,11 +465,8 @@ open class SwiftMessages: PresenterDelegate { /** Hide the current message being displayed by animating it away. */ - open func hide() { - syncQueue.async { [weak self] in - guard let strongSelf = self else { return } - strongSelf.hideCurrent() - } + open func hide(animated: Bool = true) { + hideCurrent(animated: animated) } /** @@ -315,29 +474,64 @@ open class SwiftMessages: PresenterDelegate { clear the message queue. */ open func hideAll() { - syncQueue.async { [weak self] in - guard let strongSelf = self else { return } - strongSelf.queue.removeAll() - strongSelf.hideCurrent() - } + 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 implement the `Identifiable` protocol. + views, such as `MessageView`, that adopt the `Identifiable` protocol. - Parameter id: The identifier of the message to remove. */ open func hide(id: String) { - syncQueue.async { [weak self] in - guard let strongSelf = self else { return } - if id == strongSelf.current?.id { - strongSelf.hideCurrent() + 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 } - strongSelf.queue = strongSelf.queue.filter { $0.id != id } } + 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. @@ -349,111 +543,238 @@ open class SwiftMessages: PresenterDelegate { and showing the next. Default is 0.5 seconds. */ open var pauseBetweenMessages: TimeInterval = 0.5 - - let syncQueue = DispatchQueue(label: "it.swiftkick.SwiftMessages", attributes: []) - var queue: [Presenter] = [] - var current: Presenter? = nil { + + /// 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 { - let delayTime = DispatchTime.now() + pauseBetweenMessages - syncQueue.asyncAfter(deadline: delayTime, execute: { [weak self] in - guard let strongSelf = self else { return } - strongSelf.dequeueNext() - }) + Task { [weak self] in + try? await Task.sleep(seconds: self?.pauseBetweenMessages ?? 0) + self?.dequeueNext() + } } } } - - func enqueue(presenter: Presenter) { - if presenter.config.ignoreDuplicates, let id = presenter.id { - if current?.id == id { return } - if queue.filter({ $0.id == id }).count > 0 { return } + + 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() } - queue.append(presenter) - dequeueNext() } - func dequeueNext() { - guard self.current == nil else { return } + 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 - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } - do { - try current.show { completed in - guard let strongSelf = self else { return } - guard completed else { - strongSelf.syncQueue.async(execute: { - guard let strongSelf = self else { return } - strongSelf.hide(presenter: current) - }) - return - } - strongSelf.queueAutoHide() + 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 { - strongSelf.current = nil } + } catch { + _current = nil } } - - func hideCurrent() { - guard let current = current else { return } - DispatchQueue.main.async { [weak self] in - current.hide { (completed) in - guard completed else { return } - guard let strongSelf = self else { return } - strongSelf.syncQueue.async(execute: { - guard let strongSelf = self else { return } - strongSelf.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 var autohideToken: AnyObject? - + + fileprivate weak var autohideToken: Presenter? + fileprivate func queueAutoHide() { - guard let current = current else { return } + guard let current = _current else { return } autohideToken = current if let pauseDuration = current.pauseDuration { - let delayTime = DispatchTime.now() + pauseDuration - syncQueue.asyncAfter(deadline: delayTime, execute: { [weak self] in - guard let strongSelf = self else { return } + Task { [weak self] in + try? await Task.sleep(seconds: pauseDuration) // Make sure we've still got a green light to auto-hide. - if strongSelf.autohideToken !== current { return } - strongSelf.hide(presenter: current) - }) + guard let self, self.autohideToken == current else { return } + self.internalHide(presenter: current) + } } } - - /* - MARK: - PresenterDelegate + + 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) { - syncQueue.async { [weak self] in - guard let strongSelf = self else { return } - if let current = strongSelf.current, presenter === current { - strongSelf.hideCurrent() - } - strongSelf.queue = strongSelf.queue.filter { $0 !== presenter } - } + self.internalHide(presenter: presenter) } - - func panStarted(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 } - - func panEnded(presenter: Presenter) { + + 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. @@ -529,7 +850,11 @@ extension SwiftMessages { } } 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 } } @@ -549,10 +874,10 @@ extension SwiftMessages { a set of static APIs that wrap calls to this instance. For example, `SwiftMessages.show()` is equivalent to `SwiftMessages.sharedInstance.show()`. */ - public static var sharedInstance: SwiftMessages { + nonisolated public static var sharedInstance: SwiftMessages { return globalInstance } - + public static func show(viewProvider: @escaping ViewProvider) { globalInstance.show(viewProvider: viewProvider) } @@ -569,8 +894,8 @@ extension SwiftMessages { globalInstance.show(config: config, view: view) } - public static func hide() { - globalInstance.hide() + public static func hide(animated: Bool = true) { + globalInstance.hide(animated: animated) } public static func hideAll() { @@ -580,7 +905,11 @@ extension SwiftMessages { 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 @@ -598,4 +927,24 @@ extension SwiftMessages { 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) + } } diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift new file mode 100644 index 00000000..35687e14 --- /dev/null +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -0,0 +1,407 @@ +// +// 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() + } + } +} diff --git a/SwiftMessages/Task+Extensions.swift b/SwiftMessages/Task+Extensions.swift new file mode 100644 index 00000000..9d6f1c3f --- /dev/null +++ b/SwiftMessages/Task+Extensions.swift @@ -0,0 +1,15 @@ +// +// 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)) + } +} diff --git a/SwiftMessages/Theme.swift b/SwiftMessages/Theme.swift index a31bc579..5bf10a4e 100644 --- a/SwiftMessages/Theme.swift +++ b/SwiftMessages/Theme.swift @@ -19,22 +19,22 @@ public enum Theme { /// 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" + 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)! + public var image: UIImage { + return UIImage(named: rawValue, in: Bundle.sm_frameworkBundle(), compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) } } @@ -44,22 +44,24 @@ public enum IconStyle { case `default` case light case subtle + case none /// Returns the image for the given theme - public func image(theme: Theme) -> UIImage { + 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 + 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 } } } diff --git a/SwiftMessages/TopBottomAnimation.swift b/SwiftMessages/TopBottomAnimation.swift new file mode 100644 index 00000000..49a45adc --- /dev/null +++ b/SwiftMessages/TopBottomAnimation.swift @@ -0,0 +1,227 @@ +// +// 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 + } + } +} diff --git a/SwiftMessages/TopBottomAnimationStyle.swift b/SwiftMessages/TopBottomAnimationStyle.swift new file mode 100644 index 00000000..626ad09c --- /dev/null +++ b/SwiftMessages/TopBottomAnimationStyle.swift @@ -0,0 +1,12 @@ +// +// 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 +} diff --git a/SwiftMessages/TopBottomPresentable.swift b/SwiftMessages/TopBottomPresentable.swift new file mode 100644 index 00000000..18804160 --- /dev/null +++ b/SwiftMessages/TopBottomPresentable.swift @@ -0,0 +1,49 @@ +// +// 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 + } + } +} diff --git a/SwiftMessages/UIEdgeInsets+Extensions.swift b/SwiftMessages/UIEdgeInsets+Extensions.swift new file mode 100644 index 00000000..19bbe4e3 --- /dev/null +++ b/SwiftMessages/UIEdgeInsets+Extensions.swift @@ -0,0 +1,27 @@ +// +// 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) + } +} diff --git a/SwiftMessages/UIViewController+Utils.swift b/SwiftMessages/UIViewController+Extensions.swift similarity index 67% rename from SwiftMessages/UIViewController+Utils.swift rename to SwiftMessages/UIViewController+Extensions.swift index eaa38cb6..1400e03c 100644 --- a/SwiftMessages/UIViewController+Utils.swift +++ b/SwiftMessages/UIViewController+Extensions.swift @@ -1,5 +1,5 @@ // -// UIViewController+Utils.swift +// UIViewController+Extensions.swift // SwiftMessages // // Created by Timothy Moose on 8/5/16. @@ -8,24 +8,22 @@ import UIKit -private let fullScreenStyles: [UIModalPresentationStyle] = [.fullScreen, .overFullScreen] - extension UIViewController { func sm_selectPresentationContextTopDown(_ config: SwiftMessages.Config) -> UIViewController { - let presentationStyle = config.presentationStyle - if let presented = sm_presentedFullScreenViewController() { + let topBottomStyle = config.presentationStyle.topBottomStyle + if let presented = presentedViewController { return presented.sm_selectPresentationContextTopDown(config) - } else if case .top = presentationStyle, let navigationController = sm_selectNavigationControllerTopDown() { + } else if case .top? = topBottomStyle, let navigationController = sm_selectNavigationControllerTopDown() { return navigationController - } else if case .bottom = presentationStyle, let tabBarController = sm_selectTabBarControllerTopDown() { + } else if case .bottom? = topBottomStyle, let tabBarController = sm_selectTabBarControllerTopDown() { return tabBarController } - return WindowViewController(windowLevel: self.view.window?.windowLevel ?? UIWindowLevelNormal, config: config) + return WindowViewController.newInstance(config: config) } fileprivate func sm_selectNavigationControllerTopDown() -> UINavigationController? { - if let presented = sm_presentedFullScreenViewController() { + if let presented = presentedViewController { return presented.sm_selectNavigationControllerTopDown() } else if let navigationController = self as? UINavigationController { if navigationController.sm_isVisible(view: navigationController.navigationBar) { @@ -39,7 +37,7 @@ extension UIViewController { } fileprivate func sm_selectTabBarControllerTopDown() -> UITabBarController? { - if let presented = sm_presentedFullScreenViewController() { + if let presented = presentedViewController { return presented.sm_selectTabBarControllerTopDown() } else if let navigationController = self as? UINavigationController { return navigationController.topViewController?.sm_selectTabBarControllerTopDown() @@ -51,24 +49,17 @@ extension UIViewController { } return nil } - - fileprivate func sm_presentedFullScreenViewController() -> UIViewController? { - if let presented = self.presentedViewController, fullScreenStyles.contains(presented.modalPresentationStyle) { - return presented - } - return nil - } func sm_selectPresentationContextBottomUp(_ config: SwiftMessages.Config) -> UIViewController { - let presentationStyle = config.presentationStyle + let topBottomStyle = config.presentationStyle.topBottomStyle if let parent = parent { if let navigationController = parent as? UINavigationController { - if case .top = presentationStyle, navigationController.sm_isVisible(view: navigationController.navigationBar) { + 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 = presentationStyle, tabBarController.sm_isVisible(view: tabBarController.tabBar) { + if case .bottom? = topBottomStyle, tabBarController.sm_isVisible(view: tabBarController.tabBar) { return tabBarController } return tabBarController.sm_selectPresentationContextBottomUp(config) @@ -80,7 +71,7 @@ extension UIViewController { if let parent = self.parent { return parent.sm_selectPresentationContextBottomUp(config) } else { - return WindowViewController(windowLevel: self.view.window?.windowLevel ?? UIWindowLevelNormal, config: config) + return WindowViewController.newInstance(config: config) } } return self diff --git a/SwiftMessages/UIWindow+Extensions.swift b/SwiftMessages/UIWindow+Extensions.swift new file mode 100644 index 00000000..6a626f4e --- /dev/null +++ b/SwiftMessages/UIWindow+Extensions.swift @@ -0,0 +1,34 @@ +// +// 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 + } + } +} diff --git a/SwiftMessages/Weak.swift b/SwiftMessages/Weak.swift new file mode 100644 index 00000000..a9bc4700 --- /dev/null +++ b/SwiftMessages/Weak.swift @@ -0,0 +1,16 @@ +// +// 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 + } +} diff --git a/SwiftMessages/WindowScene.swift b/SwiftMessages/WindowScene.swift new file mode 100644 index 00000000..a3bbb4ca --- /dev/null +++ b/SwiftMessages/WindowScene.swift @@ -0,0 +1,9 @@ +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 {} diff --git a/SwiftMessages/WindowViewController.swift b/SwiftMessages/WindowViewController.swift index 13e72b02..30d77f9a 100644 --- a/SwiftMessages/WindowViewController.swift +++ b/SwiftMessages/WindowViewController.swift @@ -8,52 +8,80 @@ import UIKit -class WindowViewController: UIViewController +open class WindowViewController: UIViewController { - fileprivate var window: UIWindow? - - let windowLevel: UIWindowLevel - let config: SwiftMessages.Config - - override var shouldAutorotate: Bool { + override open var shouldAutorotate: Bool { return config.shouldAutorotate } - - init(windowLevel: UIWindowLevel = UIWindowLevelNormal, config: SwiftMessages.Config) - { - self.windowLevel = windowLevel + + convenience public init() { + self.init(config: SwiftMessages.Config()) + } + + public init(config: SwiftMessages.Config) { self.config = config - let window = PassthroughWindow(frame: UIScreen.main.bounds) + let view = PassthroughView() + let window = PassthroughWindow(hitTestView: view) self.window = window super.init(nibName: nil, bundle: nil) - self.view = PassthroughView() + self.view = view window.rootViewController = self - window.windowLevel = windowLevel + window.windowLevel = config.windowLevel ?? UIWindow.Level.normal + window.overrideUserInterfaceStyle = config.overrideUserInterfaceStyle } - - func install(becomeKey: Bool) { + + 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() + window.makeKeyAndVisible() } else { window.isHidden = false } } func uninstall() { + if window?.isKeyWindow == true { + previousKeyWindow?.makeKey() + } + window?.windowScene = nil window?.isHidden = true window = nil } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public var preferredStatusBarStyle: UIStatusBarStyle { - return config.preferredStatusBarStyle ?? UIApplication.shared.statusBarStyle + override open var preferredStatusBarStyle: UIStatusBarStyle { + return config.preferredStatusBarStyle ?? super.preferredStatusBarStyle } - - override var prefersStatusBarHidden: Bool { - return UIApplication.shared.isStatusBarHidden + + 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) } } diff --git a/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.pbxproj b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f1519c40 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.pbxproj @@ -0,0 +1,446 @@ +// !$*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 */; +} diff --git a/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Contents.json b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Demo message background.colorset/Contents.json b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Demo message background.colorset/Contents.json new file mode 100644 index 00000000..15b79d47 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Demo message background.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0xE1", + "red" : "0xAC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/DemoMessage.swift b/SwiftUIDemo/SwiftUIDemo/DemoMessage.swift new file mode 100644 index 00000000..32624de1 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/DemoMessage.swift @@ -0,0 +1,23 @@ +// +// 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) + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift b/SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift new file mode 100644 index 00000000..11cf9611 --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift @@ -0,0 +1,70 @@ +// +// 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) + } +} diff --git a/SwiftUIDemo/SwiftUIDemo/DemoMessageWithButtonView.swift b/SwiftUIDemo/SwiftUIDemo/DemoMessageWithButtonView.swift new file mode 100644 index 00000000..e74172ef --- /dev/null +++ b/SwiftUIDemo/SwiftUIDemo/DemoMessageWithButtonView.swift @@ -0,0 +1,77 @@ +// +// DemoMessageWithButtonView.swift +// SwiftUIDemo +// +// Created by Timothy Moose on 1/15/24. +// + +import SwiftUI + +// A message view with a title, message and button. +struct DemoMessageWithButtonView + + + + + + + + + + + + + + + + + + + diff --git a/iMessageDemo/iMessageExtensionDemo/Info.plist b/iMessageDemo/iMessageExtensionDemo/Info.plist new file mode 100644 index 00000000..2c1114de --- /dev/null +++ b/iMessageDemo/iMessageExtensionDemo/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + iMessageExtensionDemo + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.message-payload-provider + + + diff --git a/iMessageDemo/iMessageExtensionDemo/MessagesViewController.swift b/iMessageDemo/iMessageExtensionDemo/MessagesViewController.swift new file mode 100644 index 00000000..c71afe17 --- /dev/null +++ b/iMessageDemo/iMessageExtensionDemo/MessagesViewController.swift @@ -0,0 +1,35 @@ +// +// MessagesViewController.swift +// iMessageDemo +// +// Created by Timothy Moose on 5/25/18. +// Copyright © 2018 SwiftKick Mobile. All rights reserved. +// + +import UIKit +import Messages +import SwiftMessages + +class MessagesViewController: MSMessagesAppViewController { + + @IBOutlet weak var button: UIButton! { + didSet { + button.layer.cornerRadius = 5 + } + } + + @IBAction func buttonTapped() { + let messageView: MessageView = MessageView.viewFromNib(layout: .centeredView) + messageView.configureContent(title: "Test", body: "Yep, it works!") + messageView.button?.isHidden = true + messageView.iconLabel?.isHidden = true + messageView.iconImageView?.isHidden = true + messageView.configureTheme(backgroundColor: .black, foregroundColor: .white) + messageView.configureDropShadow() + messageView.configureBackgroundView(width: 200) + var config = SwiftMessages.defaultConfig + config.presentationStyle = .center + config.presentationContext = .viewController(self) + SwiftMessages.show(config: config, view: messageView) + } +}