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/CHANGELOG.md b/CHANGELOG.md index be4ac260..1fa85286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,242 @@ # 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 diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 780b3fd6..57a9b7a5 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -195,19 +195,18 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0940; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "SwiftKick Mobile"; TargetAttributes = { 86AEDCE11D5D1DB70030232E = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 38R82CD868; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; }; }; }; buildConfigurationList = 86AEDCDD1D5D1DB70030232E /* Build configuration list for PBXProject "Demo" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -315,6 +314,7 @@ 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++"; @@ -334,6 +334,7 @@ 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; @@ -358,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; @@ -371,6 +372,7 @@ 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++"; @@ -390,6 +392,7 @@ 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; @@ -408,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"; @@ -423,12 +426,14 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - DEVELOPMENT_TEAM = 38R82CD868; + 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 = 4.2; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -438,12 +443,14 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; - DEVELOPMENT_TEAM = 38R82CD868; + 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 = 4.2; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme index 512dc8b6..db82f1fa 100644 --- a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - + + - - + @@ -22,14 +19,14 @@ - diff --git a/Demo/Demo/Base.lproj/Main.storyboard b/Demo/Demo/Base.lproj/Main.storyboard index 12449d96..c38d6707 100644 --- a/Demo/Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Base.lproj/Main.storyboard @@ -1,13 +1,10 @@ - - - - + + - - - + + @@ -35,25 +32,24 @@ - + - + - + @@ -73,22 +69,21 @@ - + - + @@ -109,22 +104,21 @@ - + - + @@ -145,22 +139,21 @@ - + - + @@ -180,22 +173,21 @@ - + - + @@ -230,28 +222,28 @@ - - + + - + - - + + - + - + @@ -266,25 +258,25 @@ - + - - + + - + - + @@ -295,7 +287,7 @@ - + @@ -303,21 +295,20 @@ - - + + - + - + @@ -333,25 +324,24 @@ - + - - + + - + - + @@ -366,26 +356,25 @@ - + - + - + - + @@ -393,7 +382,8 @@ - + + @@ -403,30 +393,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + + - @@ -439,21 +461,20 @@ - - + + - + - + @@ -469,25 +490,24 @@ - + - - + + - + - + @@ -500,7 +520,7 @@ - + @@ -508,21 +528,20 @@ - - + + - + - + @@ -535,70 +554,63 @@ - + - + - + - + - + - + - - - - - - - + + - + - - + + - + @@ -607,21 +619,20 @@ - - + + - + - - + + @@ -631,55 +642,54 @@ - + - - + + - + - + - + - + - + - + - + @@ -687,32 +697,32 @@ - + - + - + - + - + @@ -797,12 +807,14 @@ + + @@ -818,7 +830,7 @@ - + @@ -874,7 +886,7 @@ - + + @@ -72,6 +70,7 @@ + @@ -79,13 +78,25 @@ + + + + + + + + + + + + @@ -108,6 +119,20 @@ + + + + + + + + + + + + + + @@ -116,7 +141,7 @@ - + diff --git a/SwiftMessages/Resources/CenteredView.xib b/SwiftMessages/Resources/CenteredView.xib index 5a640326..a9fda2be 100644 --- a/SwiftMessages/Resources/CenteredView.xib +++ b/SwiftMessages/Resources/CenteredView.xib @@ -1,29 +1,26 @@ - - - - + + - - - + + - + - - + + - - + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -80,9 +96,13 @@ + + + + @@ -102,6 +122,20 @@ + + + + + + + + + + + + + + @@ -110,7 +144,7 @@ - + diff --git a/SwiftMessages/Resources/MessageView.xib b/SwiftMessages/Resources/MessageView.xib index 498d8b44..52ceaa7d 100644 --- a/SwiftMessages/Resources/MessageView.xib +++ b/SwiftMessages/Resources/MessageView.xib @@ -1,26 +1,23 @@ - - - - + + - - - + + - + - + - + - - + + - + @@ -97,7 +95,7 @@ - + diff --git a/SwiftMessages/Resources/StatusLine.xib b/SwiftMessages/Resources/StatusLine.xib index 0a7e0a4f..3dbef99f 100644 --- a/SwiftMessages/Resources/StatusLine.xib +++ b/SwiftMessages/Resources/StatusLine.xib @@ -1,24 +1,20 @@ - - - - + + - - - - + + - + diff --git a/SwiftMessages/Resources/TabView.xib b/SwiftMessages/Resources/TabView.xib index e7bb6da6..141467c1 100644 --- a/SwiftMessages/Resources/TabView.xib +++ b/SwiftMessages/Resources/TabView.xib @@ -1,12 +1,9 @@ - - - - + + - - - + + @@ -16,14 +13,14 @@ - + - + - + - - + + - + + @@ -79,15 +78,27 @@ + + + + + + + + + + + + @@ -110,6 +121,20 @@ + + + + + + + + + + + + + + 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 115cd543..f05ad494 100644 --- a/SwiftMessages/SwiftMessages.swift +++ b/SwiftMessages/SwiftMessages.swift @@ -15,6 +15,7 @@ private let globalInstance = SwiftMessages() It behaves like a queue, only showing one message at a time. Message views that adopt the `Identifiable` protocol (as `MessageView` does) will have duplicates removed. */ +@MainActor open class SwiftMessages { /** @@ -55,30 +56,42 @@ open class SwiftMessages { appropriate one is found. Otherwise, it is displayed in a new window at level `UIWindow.Level.normal`. Use this option to automatically display under bars, where applicable. Because this option involves a top-down - search, an 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 - `UIWindow.Level.normal` to display under the status bar and `UIWindow.Level.statusBar` - 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: 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. */ @@ -139,6 +152,19 @@ open class SwiftMessages { case indefinite(delay: TimeInterval, minimum: TimeInterval) } + /** + Specifies notification's haptic feedback to be used on `MessageView` display + */ + + /** + Specifies an optional haptic feedback to be used on `MessageView` display + */ + public enum Haptic { + case success + case warning + case error + } + /** Specifies options for dimming the background behind the message view similar to a popover view controller. @@ -207,10 +233,23 @@ open class SwiftMessages { 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 + } } /** @@ -250,6 +289,13 @@ open class SwiftMessages { */ 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` @@ -261,15 +307,22 @@ open class SwiftMessages { 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 `UIWindow.Level.normal` 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 @@ -310,19 +363,48 @@ open class SwiftMessages { */ 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: ((_ windowLevel: UIWindow.Level?, _ config: SwiftMessages.Config) -> 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. @@ -331,9 +413,7 @@ open class SwiftMessages { */ open func show(config: Config, view: UIView) { let presenter = Presenter(config: config, view: view, delegate: self) - messageQueue.sync { - enqueue(presenter: presenter) - } + enqueue(presenter: presenter) } /** @@ -360,11 +440,11 @@ open class SwiftMessages { - 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) } } @@ -385,10 +465,8 @@ open class SwiftMessages { /** Hide the current message being displayed by animating it away. */ - open func hide() { - messageQueue.sync { - hideCurrent() - } + open func hide(animated: Bool = true) { + hideCurrent(animated: animated) } /** @@ -396,12 +474,10 @@ open class SwiftMessages { clear the message queue. */ open func hideAll() { - messageQueue.sync { - queue.removeAll() - delays.ids.removeAll() - counts.removeAll() - hideCurrent() - } + queue.removeAll() + delays.removeAll() + counts.removeAll() + hideCurrent() } /** @@ -411,14 +487,12 @@ open class SwiftMessages { - Parameter id: The identifier of the message to remove. */ open func hide(id: String) { - messageQueue.sync { - if id == _current?.id { - hideCurrent() - } - queue = queue.filter { $0.id != id } - delays.ids.remove(id) - counts[id] = nil + if id == _current?.id { + hideCurrent() } + queue = queue.filter { $0.id != id } + delays.remove(id: id) + counts[id] = nil } /** @@ -427,21 +501,19 @@ open class SwiftMessages { shown from multiple code paths to ensure that all paths are ready to hide. */ open func hideCounted(id: String) { - messageQueue.sync { - if let count = counts[id] { - if count < 2 { - counts[id] = nil - } else { - counts[id] = count - 1 - return - } - } - if id == _current?.id { - hideCurrent() + if let count = counts[id] { + if count < 2 { + counts[id] = nil + } else { + counts[id] = count - 1 + return } - queue = queue.filter { $0.id != id } - delays.ids.remove(id) } + if id == _current?.id { + hideCurrent() + } + queue = queue.filter { $0.id != id } + delays.remove(id: id) } /** @@ -473,37 +545,43 @@ open class SwiftMessages { open var pauseBetweenMessages: TimeInterval = 0.5 /// Type for keeping track of delayed presentations + @MainActor fileprivate class Delays { - fileprivate var ids = Set() - fileprivate func add(presenter: Presenter) { - ids.insert(presenter.id) + presenters.insert(presenter) } @discardableResult fileprivate func remove(presenter: Presenter) -> Bool { - guard ids.contains(presenter.id) else { return false } - ids.remove(presenter.id) + 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) { - messageQueue.sync { - enqueue(presenter: presenter) - } + enqueue(presenter: presenter) } - fileprivate let messageQueue = DispatchQueue(label: "it.swiftkick.SwiftMessages", attributes: []) 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 - messageQueue.asyncAfter(deadline: delayTime) { [weak self] in + Task { [weak self] in + try? await Task.sleep(seconds: self?.pauseBetweenMessages ?? 0) self?.dequeueNext() } } @@ -513,7 +591,10 @@ open class SwiftMessages { fileprivate func enqueue(presenter: Presenter) { if presenter.config.ignoreDuplicates { counts[presenter.id] = (counts[presenter.id] ?? 0) + 1 - if _current?.id == presenter.id && _current?.isHiding == false { return } + if let _current, + _current.id == presenter.id, + !_current.isHiding, + !_current.isOrphaned { return } if queue.filter({ $0.id == presenter.id }).count > 0 { return } } func doEnqueue() { @@ -522,9 +603,10 @@ open class SwiftMessages { } if let delay = presenter.delayShow { delays.add(presenter: presenter) - messageQueue.asyncAfter(deadline: .now() + delay) { [weak self] in + 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 strongSelf = self, strongSelf.delays.remove(presenter: presenter) else { return } + guard let self, self.delays.remove(presenter: presenter) else { return } doEnqueue() } } else { @@ -533,74 +615,80 @@ open class SwiftMessages { } fileprivate func dequeueNext() { - guard self._current == nil, queue.count > 0 else { return } + guard queue.count > 0 else { return } + if let _current, !_current.isOrphaned { return } + // Sort by priority + queue = queue.sorted { left, right in + left.config.priority > right.config.priority + } let current = queue.removeFirst() self._current = current // Set `autohideToken` before the animation starts in case // the dismiss gesture begins before we've queued the autohide // block on animation completion. self.autohideToken = current - current.showDate = Date() - 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.messageQueue.sync { - strongSelf.internalHide(id: current.id) - } - return - } - if current === strongSelf.autohideToken { - strongSelf.queueAutoHide() - } + current.showDate = CACurrentMediaTime() + do { + try current.show { [weak self] completed in + guard let self else { return } + guard completed else { + self.internalHide(presenter: current) + return } - } catch { - strongSelf.messageQueue.sync { - strongSelf._current = nil + if current === self.autohideToken { + self.queueAutoHide() } } + } catch { + _current = nil } } - fileprivate func internalHide(id: String) { - if id == _current?.id { + fileprivate func internalHide(presenter: Presenter) { + if presenter == _current { hideCurrent() + } else { + queue = queue.filter { $0 != presenter } + delays.remove(presenter: presenter) } - queue = queue.filter { $0.id != id } - delays.ids.remove(id) } - - fileprivate func hideCurrent() { + + fileprivate func hideCurrent(animated: Bool = true) { guard let current = _current, !current.isHiding else { return } - let delay = current.delayHide ?? 0 - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak current] in - guard let strongCurrent = current else { return } - strongCurrent.hide { (completed) in - guard completed, let strongSelf = self, let strongCurrent = current else { return } - strongSelf.messageQueue.sync { - guard strongSelf._current === strongCurrent else { return } - strongSelf.counts[strongCurrent.id] = nil - strongSelf._current = nil - } + let action = { [weak self] in + current.hide(animated: animated) { (completed) in + guard completed, let self else { return } + guard self._current === current else { return } + self.counts[current.id] = nil + self._current = nil } } + let delay = current.delayHide ?? 0 + Task { + try? await Task.sleep(seconds: delay) + action() + } } - fileprivate weak var autohideToken: AnyObject? - + fileprivate weak var autohideToken: Presenter? + fileprivate func queueAutoHide() { guard let current = _current else { return } autohideToken = current if let pauseDuration = current.pauseDuration { - let delayTime = DispatchTime.now() + pauseDuration - messageQueue.asyncAfter(deadline: delayTime, execute: { [weak self, weak current] in - guard let strongSelf = self, let current = current 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.internalHide(id: current.id) - }) + guard let self, self.autohideToken == current else { return } + self.internalHide(presenter: current) + } + } + } + + deinit { + guard let current = _current else { return } + Task { @MainActor [current] in + current.hide(animated: true) { _ in } } } } @@ -617,11 +705,7 @@ extension SwiftMessages { - Returns: The view of type `T` if it is currently being shown or hidden. */ public func current() -> T? { - var view: T? - messageQueue.sync { - view = _current?.view as? T - } - return view + _current?.view as? T } /** @@ -631,13 +715,7 @@ extension SwiftMessages { - Returns: The view with matching id if currently being shown or hidden. */ public func current(id: String) -> T? { - var view: T? - messageQueue.sync { - if let current = _current, current.id == id { - view = current.view as? T - } - } - return view + _current?.id == id ? _current?.view as? T : nil } /** @@ -647,13 +725,7 @@ extension SwiftMessages { - Returns: The view with matching id if currently queued to be shown. */ public func queued(id: String) -> T? { - var view: T? - messageQueue.sync { - if let queued = queue.first(where: { $0.id == id }) { - view = queued.view as? T - } - } - return view + queue.first { $0.id == id }?.view as? T } /** @@ -675,16 +747,12 @@ extension SwiftMessages { extension SwiftMessages: PresenterDelegate { func hide(presenter: Presenter) { - messageQueue.sync { - self.internalHide(id: presenter.id) - } + self.internalHide(presenter: presenter) } public func hide(animator: Animator) { - messageQueue.sync { - guard let presenter = self.presenter(forAnimator: animator) else { return } - self.internalHide(id: presenter.id) - } + guard let presenter = self.presenter(forAnimator: animator) else { return } + self.internalHide(presenter: presenter) } public func panStarted(animator: Animator) { @@ -806,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) } @@ -826,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() { diff --git a/SwiftMessages/SwiftMessagesSegue.swift b/SwiftMessages/SwiftMessagesSegue.swift index 398cf3e0..35687e14 100644 --- a/SwiftMessages/SwiftMessagesSegue.swift +++ b/SwiftMessages/SwiftMessagesSegue.swift @@ -37,18 +37,32 @@ import UIKit 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. SwiftMessages relies on a view's `intrinsicContentSize` to determine the height of a message. - However, some view controllers' views does not define a good `intrinsicContentSize` - (`UINavigationController` is a common example). For these cases, there are a couple of ways - to specify the preferred height. First, you may set the `preferredContentSize` on the destination - view controller (available as "Use Preferred Explicit Size" in IB's attribute inspector). Second, - you may set `SwiftMessagesSegue.messageView.backgroundHeight`. - + 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. */ @@ -109,6 +123,45 @@ open class SwiftMessagesSegue: UIStoryboardSegue { 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 @@ -124,7 +177,7 @@ open class SwiftMessagesSegue: UIStoryboardSegue { `messageView`. This view provides configurable squircle (round) corners (see the parent class `CornerRoundingView`). */ - public var containerView = ViewControllerContainerView() + public var containerView: CornerRoundingView = CornerRoundingView() /** Specifies how the view controller's view is installed into the @@ -132,24 +185,16 @@ open class SwiftMessagesSegue: UIStoryboardSegue { */ public var containment: Containment = .content - /// 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 } - } - - /// 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 } + /** + 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() @@ -161,8 +206,12 @@ open class SwiftMessagesSegue: UIStoryboardSegue { }() override open func perform() { + (source as? WindowViewController)?.install() selfRetainer = self - destination.modalPresentationStyle = .custom + startReleaseMonitor() + if overrideModalPresentationStyle { + destination.modalPresentationStyle = .custom + } destination.transitioningDelegate = self source.present(destination, animated: true, completion: nil) } @@ -174,15 +223,26 @@ open class SwiftMessagesSegue: UIStoryboardSegue { } 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 - messageView.statusBarOffset = 0 - messageView.safeAreaTopOffset = 0 - messageView.safeAreaBottomOffset = 0 containment = .content containerView.cornerRadius = 0 containerView.roundsLeadingCorners = false @@ -231,8 +291,8 @@ extension SwiftMessagesSegue { animation.springDamping = 1 presentationStyle = .custom(animator: animation) case .centered: - containment = .backgroundVertical - messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) + containment = .background + messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) messageView.collapseLayoutMarginAdditions = true containerView.cornerRadius = 15 presentationStyle = .center @@ -243,19 +303,21 @@ extension SwiftMessagesSegue { extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let shower = TransitioningPresenter(segue: self) - messenger.defaultConfig.eventListeners.append { [unowned self] in + let hider = self.hider + messenger.defaultConfig.eventListeners.append { [weak self] in switch $0 { case .didShow: shower.completeTransition?(true) case .didHide: - if let completeTransition = self.hider.completeTransition { + if let completeTransition = hider.completeTransition { completeTransition(true) } else { - // Case where message is interinally hidden by SwiftMessages, such as with a + // 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) } - self.selfRetainer = nil + (source as? WindowViewController)?.uninstall() + self?.selfRetainer = nil default: break } } @@ -287,25 +349,14 @@ extension SwiftMessagesSegue { transitionContext.completeTransition(false) return } - if #available(iOS 12, *) {} - else if #available(iOS 11.0, *) { - // This works around a bug in iOS 11 where the safe area of `messageView` ( - // and all ancestor views) is not set except on iPhone X. By assigning `messageView` - // to a view controller, its safe area is set consistently. This bug has been resolved as - // of Xcode 10 beta 2. - segue.safeAreaWorkaroundViewController.view = segue.presenter.maskingView - } completeTransition = transitionContext.completeTransition let transitionContainer = transitionContext.containerView - // Setup the layout of the `toView` - do { - toView.translatesAutoresizingMaskIntoConstraints = false - segue.containerView.addSubview(toView) - toView.topAnchor.constraint(equalTo: segue.containerView.topAnchor).isActive = true - toView.bottomAnchor.constraint(equalTo: segue.containerView.bottomAnchor).isActive = true - toView.leftAnchor.constraint(equalTo: segue.containerView.leftAnchor).isActive = true - toView.rightAnchor.constraint(equalTo: segue.containerView.rightAnchor).isActive = true - } + 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: @@ -315,7 +366,15 @@ extension SwiftMessagesSegue { case .backgroundVertical: segue.messageView.installBackgroundVerticalView(segue.containerView) } - segue.containerView.viewController = transitionContext.viewController(forKey: .to) + 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) } 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/TopBottomAnimation.swift b/SwiftMessages/TopBottomAnimation.swift index 59196ea1..49a45adc 100644 --- a/SwiftMessages/TopBottomAnimation.swift +++ b/SwiftMessages/TopBottomAnimation.swift @@ -8,34 +8,40 @@ import UIKit +@MainActor public class TopBottomAnimation: NSObject, Animator { - public enum Style { - case top - case bottom - } - public weak var delegate: AnimationDelegate? - public let style: Style + public let style: TopBottomAnimationStyle + + public var showDuration: TimeInterval = 0.4 + + public var hideDuration: TimeInterval = 0.2 - open var springDamping: CGFloat = 0.8 + public var springDamping: CGFloat = 0.8 - open var closeSpeedThreshold: CGFloat = 750.0; + public var closeSpeedThreshold: CGFloat = 750.0; - open var closePercentThreshold: CGFloat = 0.33; + public var closePercentThreshold: CGFloat = 0.33; - open var closeAbsoluteThreshold: CGFloat = 75.0; + 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: Style) { + public init(style: TopBottomAnimationStyle) { self.style = style } - init(style: Style, delegate: AnimationDelegate) { + init(style: TopBottomAnimationStyle, delegate: AnimationDelegate) { self.style = style self.delegate = delegate } @@ -50,7 +56,7 @@ public class TopBottomAnimation: NSObject, Animator { NotificationCenter.default.removeObserver(self) let view = context.messageView self.context = context - UIView.animate(withDuration: hideDuration!, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { + UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { switch self.style { case .top: view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height) @@ -67,10 +73,6 @@ public class TopBottomAnimation: NSObject, Animator { }) } - public var showDuration: TimeInterval? { return 0.4 } - - public var hideDuration: TimeInterval? { return 0.2 } - func install(context: AnimationContext) { let view = context.messageView let container = context.containerView @@ -86,9 +88,9 @@ public class TopBottomAnimation: NSObject, Animator { view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true switch style { case .top: - view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).isActive = true + view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).with(priority: UILayoutPriority(200)).isActive = true case .bottom: - view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset).isActive = true + 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() @@ -102,12 +104,10 @@ public class TopBottomAnimation: NSObject, Animator { view.transform = CGAffineTransform(translationX: 0, y: animationDistance) } if context.interactiveHide { - let pan = UIPanGestureRecognizer() - pan.addTarget(self, action: #selector(pan(_:))) if let view = view as? BackgroundViewable { - view.backgroundView.addGestureRecognizer(pan) + view.backgroundView.addGestureRecognizer(panGestureRecognizer) } else { - view.addGestureRecognizer(pan) + view.addGestureRecognizer(panGestureRecognizer) } } if let view = view as? BackgroundViewable, @@ -126,9 +126,7 @@ public class TopBottomAnimation: NSObject, Animator { guard let adjustable = messageView as? MarginAdjustable & UIView, let context = context else { return } adjustable.preservesSuperviewLayoutMargins = false - if #available(iOS 11, *) { - adjustable.insetsLayoutMarginsFromSafeArea = false - } + adjustable.insetsLayoutMarginsFromSafeArea = false var layoutMargins = adjustable.defaultMarginAdjustment(context: context) switch style { case .top: @@ -148,7 +146,7 @@ public class TopBottomAnimation: NSObject, Animator { // 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: { + 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. 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+Utils.swift b/SwiftMessages/UIEdgeInsets+Extensions.swift similarity index 57% rename from SwiftMessages/UIEdgeInsets+Utils.swift rename to SwiftMessages/UIEdgeInsets+Extensions.swift index fe3a2e04..19bbe4e3 100644 --- a/SwiftMessages/UIEdgeInsets+Utils.swift +++ b/SwiftMessages/UIEdgeInsets+Extensions.swift @@ -1,5 +1,5 @@ // -// UIEdgeInsets+Utils.swift +// UIEdgeInsets+Extensions.swift // SwiftMessages // // Created by Timothy Moose on 5/23/18. @@ -16,4 +16,12 @@ extension UIEdgeInsets { 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 72% rename from SwiftMessages/UIViewController+Utils.swift rename to SwiftMessages/UIViewController+Extensions.swift index 984e9618..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 topBottomStyle = config.presentationStyle.topBottomStyle - if let presented = sm_presentedFullScreenViewController() { + if let presented = presentedViewController { return presented.sm_selectPresentationContextTopDown(config) } else if case .top? = topBottomStyle, let navigationController = sm_selectNavigationControllerTopDown() { return navigationController } else if case .bottom? = topBottomStyle, let tabBarController = sm_selectTabBarControllerTopDown() { return tabBarController } - return WindowViewController.newInstance(windowLevel: self.view.window?.windowLevel, 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,13 +49,6 @@ 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 topBottomStyle = config.presentationStyle.topBottomStyle @@ -80,7 +71,7 @@ extension UIViewController { if let parent = self.parent { return parent.sm_selectPresentationContextBottomUp(config) } else { - return WindowViewController.newInstance(windowLevel: self.view.window?.windowLevel, config: config) + return WindowViewController.newInstance(config: config) } } return self @@ -94,17 +85,3 @@ extension UIViewController { return true } } - -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. - var topBottomStyle: TopBottomAnimation.Style? { - switch self { - case .top: return .top - case .bottom: return .bottom - case .custom(let animator): return (animator as? TopBottomAnimation)?.style - case .center: return nil - } - } -} 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/ViewControllerContainerView.swift b/SwiftMessages/ViewControllerContainerView.swift deleted file mode 100644 index ded4c546..00000000 --- a/SwiftMessages/ViewControllerContainerView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ViewControllerContainerView.swift -// SwiftMessages -// -// Created by Timothy Moose on 8/4/18. -// Copyright © 2018 SwiftKick Mobile. All rights reserved. -// - -import UIKit - -/// A subclass of `CornerRoundingView` intended as the container view -/// of a view controller's view. It's job is to respect the view controller's -/// `preferredContentSize.height` property. (SwiftMessages does not currently -/// consider the value of `preferredContentSize.width`, but this may change in the future). -open class ViewControllerContainerView: CornerRoundingView { - - open internal(set) weak var viewController: UIViewController? - - open override var intrinsicContentSize: CGSize { - if let preferredHeight = viewController?.preferredContentSize.height, - preferredHeight > 0 { - return CGSize(width: UIView.noIntrinsicMetric, height: preferredHeight) - } - return super.intrinsicContentSize - } -} 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 a2ae7d3f..30d77f9a 100644 --- a/SwiftMessages/WindowViewController.swift +++ b/SwiftMessages/WindowViewController.swift @@ -10,37 +10,52 @@ import UIKit open class WindowViewController: UIViewController { - fileprivate var window: UIWindow? - - let windowLevel: UIWindow.Level - let config: SwiftMessages.Config - override open var shouldAutorotate: Bool { return config.shouldAutorotate } - - public init(windowLevel: UIWindow.Level?, config: SwiftMessages.Config) - { - self.windowLevel = windowLevel ?? UIWindow.Level.normal + + 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 ?? UIWindow.Level.normal + 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 } @@ -52,10 +67,21 @@ open class WindowViewController: UIViewController override open var preferredStatusBarStyle: UIStatusBarStyle { return config.preferredStatusBarStyle ?? super.preferredStatusBarStyle } + + open override var prefersStatusBarHidden: Bool { + return config.prefersStatusBarHidden ?? super.prefersStatusBarHidden + } + + // MARK: - Variables + + private var window: UIWindow? + private weak var previousKeyWindow: UIWindow? + + let config: SwiftMessages.Config } extension WindowViewController { - static func newInstance(windowLevel: UIWindow.Level?, config: SwiftMessages.Config) -> WindowViewController { - return config.windowViewController?(windowLevel, config) ?? WindowViewController(windowLevel: windowLevel, config: config) + 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