├── .gitignore ├── CHANGELOG.md ├── Demo ├── .Podfile ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Demo.xcscheme ├── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x-1.png │ │ │ ├── Icon-App-20x20@2x-2.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x-1.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x-1.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@2x-1.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Icon-App-iTunes.png │ │ ├── Contents.json │ │ ├── iconSwiftMessages.imageset │ │ │ ├── Contents.json │ │ │ └── iconSwiftMessages.pdf │ │ ├── puppies.imageset │ │ │ ├── Contents.json │ │ │ └── puppies.png │ │ └── splashBanner.imageset │ │ │ ├── Contents.json │ │ │ └── splashBanner.pdf │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CountedMessageView.swift │ ├── CountedViewController.swift │ ├── ExploreViewController.swift │ ├── Info.plist │ ├── TacoDialogView.swift │ ├── TacoDialogView.xib │ ├── Utils.swift │ ├── ViewController.swift │ └── ViewControllersViewController.swift ├── appetize.png └── demo.png ├── Design ├── SwiftMessages Demo App Icon.sketch ├── SwiftMessagesDesign.sketch ├── SwiftMessagesSegue.gif ├── SwiftMessagesSegueCreate.png └── swiftmessages.png ├── LICENSE.md ├── Package.swift ├── README.md ├── SwiftMessages.podspec ├── SwiftMessages.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── SwiftMessages.xcscheme ├── SwiftMessages ├── AccessibleMessage.swift ├── Animator.swift ├── BackgroundViewable.swift ├── BaseView.swift ├── CALayer+Extensions.swift ├── CornerRoundingView.swift ├── Error.swift ├── HapticMessage.swift ├── Identifiable.swift ├── Info.plist ├── KeyboardTrackingView.swift ├── MarginAdjustable+Extensions.swift ├── MarginAdjustable.swift ├── MaskingView.swift ├── MessageGeometryProxy.swift ├── MessageHostingView.swift ├── MessageView.swift ├── MessageViewConvertible.swift ├── NSBundle+Extensions.swift ├── NSLayoutConstraint+Extensions.swift ├── PassthroughView.swift ├── PassthroughWindow.swift ├── PhysicsAnimation.swift ├── PhysicsPanHandler.swift ├── Presenter.swift ├── Resources │ ├── CardView.xib │ ├── CenteredView.xib │ ├── MessageView.xib │ ├── StatusLine.xib │ ├── TabView.xib │ ├── errorIcon.png │ ├── errorIcon@2x.png │ ├── errorIcon@3x.png │ ├── errorIconLight.png │ ├── errorIconLight@2x.png │ ├── errorIconLight@3x.png │ ├── errorIconSubtle.png │ ├── errorIconSubtle@2x.png │ ├── errorIconSubtle@3x.png │ ├── infoIcon.png │ ├── infoIcon@2x.png │ ├── infoIcon@3x.png │ ├── infoIconLight.png │ ├── infoIconLight@2x.png │ ├── infoIconLight@3x.png │ ├── infoIconSubtle.png │ ├── infoIconSubtle@2x.png │ ├── infoIconSubtle@3x.png │ ├── successIcon.png │ ├── successIcon@2x.png │ ├── successIcon@3x.png │ ├── successIconLight.png │ ├── successIconLight@2x.png │ ├── successIconLight@3x.png │ ├── successIconSubtle.png │ ├── successIconSubtle@2x.png │ ├── successIconSubtle@3x.png │ ├── warningIcon.png │ ├── warningIcon@2x.png │ ├── warningIcon@3x.png │ ├── warningIconLight.png │ ├── warningIconLight@2x.png │ ├── warningIconLight@3x.png │ ├── warningIconSubtle.png │ ├── warningIconSubtle@2x.png │ └── warningIconSubtle@3x.png ├── SwiftMessageModifier.swift ├── SwiftMessages.Config+Extensions.swift ├── SwiftMessages.h ├── SwiftMessages.swift ├── SwiftMessagesSegue.swift ├── Task+Extensions.swift ├── Theme.swift ├── TopBottomAnimation.swift ├── TopBottomAnimationStyle.swift ├── TopBottomPresentable.swift ├── UIEdgeInsets+Extensions.swift ├── UIViewController+Extensions.swift ├── UIWindow+Extensions.swift ├── Weak.swift ├── WindowScene.swift └── WindowViewController.swift ├── SwiftMessagesTests ├── Info.plist └── SwiftMessagesTests.swift ├── SwiftUIDemo ├── SwiftUIDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── SwiftUIDemo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── Demo message background.colorset │ │ └── Contents.json │ ├── DemoMessage.swift │ ├── DemoMessageView.swift │ ├── DemoMessageWithButtonView.swift │ ├── DemoView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── SwiftUIDemoApp.swift ├── ViewControllers.md └── iMessageDemo ├── .Podfile ├── Podfile ├── Podfile.lock ├── Pods ├── Local Podspecs │ └── SwiftMessages.podspec.json ├── Manifest.lock ├── Pods.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Target Support Files │ ├── Pods-iMessageDemo │ ├── Pods-iMessageDemo-Info.plist │ ├── Pods-iMessageDemo-acknowledgements.markdown │ ├── Pods-iMessageDemo-acknowledgements.plist │ ├── Pods-iMessageDemo-dummy.m │ ├── Pods-iMessageDemo-frameworks-Debug-input-files.xcfilelist │ ├── Pods-iMessageDemo-frameworks-Debug-output-files.xcfilelist │ ├── Pods-iMessageDemo-frameworks-Release-input-files.xcfilelist │ ├── Pods-iMessageDemo-frameworks-Release-output-files.xcfilelist │ ├── Pods-iMessageDemo-frameworks.sh │ ├── Pods-iMessageDemo-umbrella.h │ ├── Pods-iMessageDemo.debug.xcconfig │ ├── Pods-iMessageDemo.modulemap │ └── Pods-iMessageDemo.release.xcconfig │ ├── Pods-iMessageExtensionDemo │ ├── Pods-iMessageExtensionDemo-Info.plist │ ├── Pods-iMessageExtensionDemo-acknowledgements.markdown │ ├── Pods-iMessageExtensionDemo-acknowledgements.plist │ ├── Pods-iMessageExtensionDemo-dummy.m │ ├── Pods-iMessageExtensionDemo-umbrella.h │ ├── Pods-iMessageExtensionDemo.debug.xcconfig │ ├── Pods-iMessageExtensionDemo.modulemap │ └── Pods-iMessageExtensionDemo.release.xcconfig │ └── SwiftMessages │ ├── ResourceBundle-SwiftMessages-SwiftMessages-Info.plist │ ├── ResourceBundle-SwiftMessages_SwiftMessages-SwiftMessages-Info.plist │ ├── SwiftMessages-Info.plist │ ├── SwiftMessages-dummy.m │ ├── SwiftMessages-prefix.pch │ ├── SwiftMessages-umbrella.h │ ├── SwiftMessages.debug.xcconfig │ ├── SwiftMessages.modulemap │ └── SwiftMessages.release.xcconfig ├── iMessageDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── iMessageExtensionDemo.xcscheme ├── iMessageDemo.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── iMessageDemo ├── AppDelegate.swift ├── Info.plist ├── LaunchScreen.storyboard └── Main.storyboard └── iMessageExtensionDemo ├── Base.lproj └── MainInterface.storyboard ├── Info.plist └── MessagesViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | .swiftpm/ 40 | 41 | .DS_Store 42 | *.mobileprovision 43 | *.cer 44 | *.certSigningRequest 45 | *.p12 46 | *.pem 47 | *.pkey 48 | screenshots 49 | rvm.env 50 | .build -------------------------------------------------------------------------------- /Demo/.Podfile: -------------------------------------------------------------------------------- 1 | target 'Demo' do 2 | use_frameworks! 3 | workspace 'Demo.xcworkspace' 4 | xcodeproj 'Demo.xcodeproj' 5 | pod 'SwiftMessages/App', :path => '../' 6 | pod 'SwiftMessages/SegueExtras', :path => '../' 7 | end 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by Tim Moose on 8/11/16. 6 | // Copyright © 2016 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let brandColor = UIColor(red: 42/255.0, green: 168/255.0, blue: 250/255.0, alpha: 1) 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 19 | window?.tintColor = brandColor 20 | UISwitch.appearance().onTintColor = brandColor 21 | return true 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x-1.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x-1.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x-1.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-20x20@2x-2.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x-1.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-iTunes.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-2.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x-1.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconSwiftMessages.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/iconSwiftMessages.imageset/iconSwiftMessages.pdf -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/puppies.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "puppies.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/puppies.imageset/puppies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/puppies.imageset/puppies.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/splashBanner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "splashBanner.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/splashBanner.imageset/splashBanner.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/Demo/Assets.xcassets/splashBanner.imageset/splashBanner.pdf -------------------------------------------------------------------------------- /Demo/Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Demo/Demo/CountedMessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountedMessageView.swift 3 | // Demo 4 | // 5 | // Created by Timothy Moose on 8/25/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftMessages 11 | 12 | class CountedMessageView: UIView, Identifiable { 13 | 14 | @IBOutlet weak var countLabel: UILabel! 15 | 16 | var id: String { 17 | return "counted" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Demo/Demo/CountedViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountedViewController.swift 3 | // Demo 4 | // 5 | // Created by Timothy Moose on 8/25/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftMessages 11 | 12 | class CountedViewController: UIViewController { 13 | 14 | @IBOutlet weak var descriptionLabel: UILabel! 15 | @IBOutlet var messageView: CountedMessageView! 16 | @IBOutlet weak var messageContainer: UIView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | descriptionLabel.configureBodyTextStyle() 21 | descriptionLabel.configureCodeStyle(on: "show()") 22 | descriptionLabel.configureCodeStyle(on: "hideCounted(id:)") 23 | } 24 | 25 | @IBAction func show() { 26 | var config = SwiftMessages.defaultConfig 27 | config.presentationStyle = .center 28 | config.duration = .forever 29 | config.presentationContext = .view(messageContainer) 30 | SwiftMessages.show(config: config, view: messageView) 31 | updateCountLabel() 32 | } 33 | 34 | @IBAction func hide() { 35 | SwiftMessages.hideCounted(id: messageView.id) 36 | updateCountLabel() 37 | } 38 | 39 | private func updateCountLabel() { 40 | let count = SwiftMessages.count(id: messageView.id) 41 | let numberFormatter = NumberFormatter() 42 | numberFormatter.numberStyle = .spellOut 43 | messageView.countLabel.text = numberFormatter.string(from: NSNumber(value: count))?.uppercased() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Demo/Demo/ExploreViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExploreViewController.swift 3 | // Demo 4 | // 5 | // Created by Tim Moose on 8/13/16. 6 | // Copyright © 2016 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftMessages 11 | 12 | class ExploreViewController: UITableViewController, UITextFieldDelegate { 13 | 14 | @IBAction func show(_ sender: AnyObject) { 15 | 16 | // View setup 17 | 18 | let view: MessageView 19 | switch layout.selectedSegmentIndex { 20 | case 1: 21 | view = MessageView.viewFromNib(layout: .cardView) 22 | case 2: 23 | view = MessageView.viewFromNib(layout: .tabView) 24 | case 3: 25 | view = MessageView.viewFromNib(layout: .statusLine) 26 | default: 27 | view = try! SwiftMessages.viewFromNib() 28 | } 29 | 30 | view.configureContent(title: titleText.text, body: bodyText.text, iconImage: nil, iconText: nil, buttonImage: nil, buttonTitle: "Hide", buttonTapHandler: { _ in SwiftMessages.hide() }) 31 | 32 | let iconStyle: IconStyle 33 | switch self.iconStyle.selectedSegmentIndex { 34 | case 1: 35 | iconStyle = .light 36 | case 2: 37 | iconStyle = .subtle 38 | default: 39 | iconStyle = .default 40 | } 41 | 42 | switch theme.selectedSegmentIndex { 43 | case 0: 44 | view.configureTheme(.info, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) 45 | view.accessibilityPrefix = "info" 46 | case 1: 47 | view.configureTheme(.success, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) 48 | view.accessibilityPrefix = "success" 49 | case 2: 50 | view.configureTheme(.warning, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) 51 | view.accessibilityPrefix = "warning" 52 | case 3: 53 | view.configureTheme(.error, iconStyle: iconStyle, includeHaptic: hapticFeedback.isOn) 54 | view.accessibilityPrefix = "error" 55 | default: 56 | let iconText = ["🐸", "🐷", "🐬", "🐠", "🐍", "🐹", "🐼"].randomElement() 57 | view.configureTheme(backgroundColor: UIColor.purple, foregroundColor: UIColor.white, iconImage: nil, iconText: iconText) 58 | view.button?.setImage(Icon.errorSubtle.image, for: .normal) 59 | view.button?.setTitle(nil, for: .normal) 60 | view.button?.backgroundColor = UIColor.clear 61 | view.button?.tintColor = UIColor.green.withAlphaComponent(0.7) 62 | } 63 | 64 | if dropShadow.isOn { 65 | view.configureDropShadow() 66 | } 67 | 68 | if !showButton.isOn { 69 | view.button?.isHidden = true 70 | } 71 | 72 | if !showIcon.isOn { 73 | view.iconImageView?.isHidden = true 74 | view.iconLabel?.isHidden = true 75 | } 76 | 77 | if !showTitle.isOn { 78 | view.titleLabel?.isHidden = true 79 | } 80 | 81 | if !showBody.isOn { 82 | view.bodyLabel?.isHidden = true 83 | } 84 | 85 | // Config setup 86 | 87 | var config = SwiftMessages.defaultConfig 88 | 89 | switch presentationStyle.selectedSegmentIndex { 90 | case 1: 91 | config.presentationStyle = .bottom 92 | case 2: 93 | config.presentationStyle = .center 94 | default: 95 | break 96 | } 97 | 98 | switch presentationContext.selectedSegmentIndex { 99 | case 1: 100 | config.presentationContext = .window(windowLevel: UIWindow.Level.normal) 101 | case 2: 102 | config.presentationContext = .window(windowLevel: UIWindow.Level.statusBar) 103 | default: 104 | break 105 | } 106 | 107 | switch duration.selectedSegmentIndex { 108 | case 1: 109 | config.duration = .forever 110 | case 2: 111 | config.duration = .seconds(seconds: 1) 112 | case 3: 113 | config.duration = .seconds(seconds: 5) 114 | default: 115 | break 116 | } 117 | 118 | switch dimMode.selectedSegmentIndex { 119 | case 1: 120 | config.dimMode = .gray(interactive: true) 121 | case 2: 122 | config.dimMode = .color(color: #colorLiteral(red: 0.1019607857, green: 0.2784313858, blue: 0.400000006, alpha: 0.7477525685), interactive: true) 123 | case 3: 124 | config.dimMode = .blur(style: .dark, alpha: 1.0, interactive: true) 125 | default: 126 | break 127 | } 128 | 129 | config.shouldAutorotate = self.autoRotate.isOn 130 | 131 | config.interactiveHide = interactiveHide.isOn 132 | 133 | // Set status bar style unless using card view (since it doesn't 134 | // go behind the status bar). 135 | if case .top = config.presentationStyle, layout.selectedSegmentIndex != 1 { 136 | switch theme.selectedSegmentIndex { 137 | case 1...4: 138 | config.preferredStatusBarStyle = .lightContent 139 | default: 140 | break 141 | } 142 | } 143 | 144 | if view.defaultHaptic == nil && hapticFeedback.isOn { 145 | config.haptic = .success 146 | } 147 | 148 | // Show 149 | SwiftMessages.show(config: config, view: view) 150 | } 151 | 152 | @IBAction func hide(_ sender: AnyObject) { 153 | SwiftMessages.hide() 154 | } 155 | 156 | @IBOutlet weak var presentationStyle: UISegmentedControl! 157 | @IBOutlet weak var presentationContext: UISegmentedControl! 158 | @IBOutlet weak var duration: UISegmentedControl! 159 | @IBOutlet weak var dimMode: UISegmentedControl! 160 | @IBOutlet weak var interactiveHide: UISwitch! 161 | @IBOutlet weak var hapticFeedback: UISwitch! 162 | @IBOutlet weak var layout: UISegmentedControl! 163 | @IBOutlet weak var theme: UISegmentedControl! 164 | @IBOutlet weak var iconStyle: UISegmentedControl! 165 | @IBOutlet weak var autoRotate: UISwitch! 166 | @IBOutlet weak var dropShadow: UISwitch! 167 | @IBOutlet weak var titleText: UITextField! 168 | @IBOutlet weak var bodyText: UITextField! 169 | @IBOutlet weak var showButton: UISwitch! 170 | @IBOutlet weak var showIcon: UISwitch! 171 | @IBOutlet weak var showTitle: UISwitch! 172 | @IBOutlet weak var showBody: UISwitch! 173 | 174 | override func viewDidLoad() { 175 | super.viewDidLoad() 176 | titleText.delegate = self 177 | bodyText.delegate = self 178 | } 179 | 180 | /* 181 | MARK: - UITextFieldDelegate 182 | */ 183 | 184 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 185 | textField.resignFirstResponder() 186 | return true 187 | } 188 | 189 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 190 | return true 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Demo/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | SM Demo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | UIInterfaceOrientationPortraitUpsideDown 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Demo/Demo/TacoDialogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TacoDialogView.swift 3 | // Demo 4 | // 5 | // Created by Tim Moose on 8/12/16. 6 | // Copyright © 2016 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftMessages 11 | 12 | class TacoDialogView: MessageView { 13 | 14 | fileprivate static var tacoTitles = [ 15 | 1 : "Just one, Please", 16 | 2 : "Make it two!", 17 | 3 : "Three!!!", 18 | 4 : "Cuatro!!!!", 19 | ] 20 | 21 | var getTacosAction: ((_ count: Int) -> Void)? 22 | var cancelAction: (() -> Void)? 23 | 24 | fileprivate var count = 1 { 25 | didSet { 26 | iconLabel?.text = String(repeating: "🌮", count: count)//String(count: count, repeatedValue: ) 27 | bodyLabel?.text = TacoDialogView.tacoTitles[count] ?? "\(count)" + String(repeating: "!", count: count) 28 | } 29 | } 30 | 31 | @IBAction func getTacos() { 32 | getTacosAction?(Int(tacoSlider.value)) 33 | } 34 | 35 | @IBAction func cancel() { 36 | cancelAction?() 37 | } 38 | 39 | @IBOutlet weak var tacoSlider: UISlider! 40 | 41 | @IBAction func tacoSliderSlid(_ slider: UISlider) { 42 | count = Int(slider.value) 43 | } 44 | 45 | @IBAction func tacoSliderFinished(_ slider: UISlider) { 46 | slider.setValue(Float(count), animated: true) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Demo/Demo/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // Demo 4 | // 5 | // Created by Timothy Moose on 8/25/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UILabel { 12 | 13 | func configureBodyTextStyle() { 14 | let bodyStyle = NSMutableParagraphStyle() 15 | bodyStyle.lineSpacing = 5.0 16 | attributedText = NSAttributedString(string: text ?? "", attributes: [NSAttributedString.Key.paragraphStyle : bodyStyle]) 17 | } 18 | 19 | func configureCodeStyle(on substring: String?) { 20 | var attributes: [NSAttributedString.Key : Any] = [:] 21 | let codeFont = UIFont(name: "CourierNewPSMT", size: font.pointSize)! 22 | attributes[NSAttributedString.Key.font] = codeFont 23 | attributes[NSAttributedString.Key.backgroundColor] = UIColor(white: 0.96, alpha: 1) 24 | attributedText = attributedText?.setAttributes(attributes: attributes, onSubstring: substring) 25 | } 26 | } 27 | 28 | extension NSAttributedString { 29 | 30 | public func setAttributes(attributes: [NSAttributedString.Key : Any], onSubstring substring: String?) -> NSAttributedString { 31 | let mutableSelf = NSMutableAttributedString(attributedString: self) 32 | if let substring = substring { 33 | var range = NSRange() 34 | repeat { 35 | let length = mutableSelf.length 36 | let start = range.location + range.length 37 | let remainingLength = length - start 38 | let remainingRange = NSRange(location: start, length: remainingLength) 39 | range = (mutableSelf.string as NSString).range(of: substring, options: .caseInsensitive, range: remainingRange) 40 | NSAttributedString.set(attributes: attributes, in: range, of: mutableSelf) 41 | } while range.length > 0 42 | } else { 43 | let range = NSRange(location: 0, length: mutableSelf.length) 44 | NSAttributedString.set(attributes: attributes, in: range, of: mutableSelf) 45 | } 46 | return mutableSelf 47 | } 48 | 49 | private static func set(attributes newAttributes: [NSAttributedString.Key : Any], in range: NSRange, of mutableString: NSMutableAttributedString) { 50 | if range.length > 0 { 51 | var attributes = mutableString.attributes(at: range.location, effectiveRange: nil) 52 | for (key, value) in newAttributes { 53 | attributes.updateValue(value, forKey: key) 54 | } 55 | mutableString.setAttributes(attributes, range: range) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Demo/ViewControllersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllersViewController.swift 3 | // Demo 4 | // 5 | // Created by Timothy Moose on 7/28/18. 6 | // Copyright © 2018 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftMessages 11 | 12 | class ViewControllersViewController: UIViewController { 13 | @objc @IBAction private func dismissPresented(segue: UIStoryboardSegue) { 14 | dismiss(animated: true, completion: nil) 15 | } 16 | } 17 | 18 | class SwiftMessagesTopSegue: SwiftMessagesSegue { 19 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 20 | super.init(identifier: identifier, source: source, destination: destination) 21 | configure(layout: .topMessage) 22 | } 23 | } 24 | 25 | class SwiftMessagesTopCardSegue: SwiftMessagesSegue { 26 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 27 | super.init(identifier: identifier, source: source, destination: destination) 28 | configure(layout: .topCard) 29 | } 30 | } 31 | 32 | class SwiftMessagesTopTabSegue: SwiftMessagesSegue { 33 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 34 | super.init(identifier: identifier, source: source, destination: destination) 35 | configure(layout: .topTab) 36 | } 37 | } 38 | 39 | class SwiftMessagesBottomSegue: SwiftMessagesSegue { 40 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 41 | super.init(identifier: identifier, source: source, destination: destination) 42 | configure(layout: .bottomMessage) 43 | } 44 | } 45 | 46 | class SwiftMessagesBottomCardSegue: SwiftMessagesSegue { 47 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 48 | super.init(identifier: identifier, source: source, destination: destination) 49 | configure(layout: .bottomCard) 50 | } 51 | } 52 | 53 | class SwiftMessagesBottomTabSegue: SwiftMessagesSegue { 54 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 55 | super.init(identifier: identifier, source: source, destination: destination) 56 | configure(layout: .bottomTab) 57 | } 58 | } 59 | 60 | class SwiftMessagesCenteredSegue: SwiftMessagesSegue { 61 | override public init(identifier: String?, source: UIViewController, destination: UIViewController) { 62 | super.init(identifier: identifier, source: source, destination: destination) 63 | configure(layout: .centered) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Demo/appetize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/appetize.png -------------------------------------------------------------------------------- /Demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Demo/demo.png -------------------------------------------------------------------------------- /Design/SwiftMessages Demo App Icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Design/SwiftMessages Demo App Icon.sketch -------------------------------------------------------------------------------- /Design/SwiftMessagesDesign.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Design/SwiftMessagesDesign.sketch -------------------------------------------------------------------------------- /Design/SwiftMessagesSegue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Design/SwiftMessagesSegue.gif -------------------------------------------------------------------------------- /Design/SwiftMessagesSegueCreate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Design/SwiftMessagesSegueCreate.png -------------------------------------------------------------------------------- /Design/swiftmessages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/Design/swiftmessages.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 SwiftKick Mobile LLC 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftMessages", 6 | platforms: [ 7 | .iOS("13.0") 8 | ], 9 | products: [ 10 | .library(name: "SwiftMessages", targets: ["SwiftMessages"]), 11 | .library(name: "SwiftMessages-Dynamic", type: .dynamic, targets: ["SwiftMessages"]) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "SwiftMessages", 16 | path: "SwiftMessages", 17 | exclude: [ 18 | "Info.plist", 19 | ], 20 | resources: [.process("Resources")] 21 | ) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /SwiftMessages.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'SwiftMessages' 3 | spec.version = '10.0.1' 4 | spec.license = { :type => 'MIT' } 5 | spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages' 6 | spec.authors = { 'Timothy Moose' => 'tim@swiftkickmobile.com' } 7 | spec.summary = 'A very flexible message bar for iOS written in Swift.' 8 | spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => spec.version} 9 | spec.platform = :ios, '13.0' 10 | spec.swift_version = '5.0' 11 | spec.ios.deployment_target = '13.0' 12 | spec.framework = 'UIKit' 13 | spec.requires_arc = true 14 | spec.default_subspec = 'App' 15 | 16 | spec.subspec 'App' do |app| 17 | app.source_files = 'SwiftMessages/**/*.swift' 18 | app.resource_bundles = {'SwiftMessages' => ['SwiftMessages/Resources/*.*']} 19 | end 20 | 21 | spec.subspec 'AppExtension' do |ext| 22 | ext.source_files = 'SwiftMessages/**/*.swift' 23 | ext.exclude_files = 'SwiftMessages/**/SegueConvenienceClasses.swift' 24 | ext.resource_bundles = {'SwiftMessages_SwiftMessages' => ['SwiftMessages/Resources/**/*.*']} 25 | 26 | # For app extensions, disabling code paths using unavailable API 27 | ext.pod_target_xcconfig = { 28 | 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'SWIFTMESSAGES_APP_EXTENSIONS', 29 | 'GCC_PREPROCESSOR_DEFINITIONS' => 'SWIFTMESSAGES_APP_EXTENSIONS=1' 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /SwiftMessages.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftMessages.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftMessages.xcodeproj/xcshareddata/xcschemes/SwiftMessages.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SwiftMessages/AccessibleMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibleMessage.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 3/11/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Message views that conform to `AccessibleMessage` will have proper accessibility behavior when displaying messages. 13 | `MessageView` implements this protocol. 14 | */ 15 | public protocol AccessibleMessage { 16 | var accessibilityMessage: String? { get } 17 | var accessibilityElement: NSObject? { get } 18 | var additionalAccessibilityElements: [NSObject]? { get } 19 | } 20 | -------------------------------------------------------------------------------- /SwiftMessages/Animator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animator.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/4/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias AnimationCompletion = (_ completed: Bool) -> Void 12 | 13 | @MainActor 14 | public protocol AnimationDelegate: AnyObject { 15 | func hide(animator: Animator) 16 | func panStarted(animator: Animator) 17 | func panEnded(animator: Animator) 18 | } 19 | 20 | /** 21 | An option set representing the known types of safe area conflicts 22 | that could require margin adjustments on the message view in order to 23 | get the layouts to look right. 24 | */ 25 | public struct SafeZoneConflicts: OptionSet { 26 | public let rawValue: Int 27 | 28 | public init(rawValue: Int) { 29 | self.rawValue = rawValue 30 | } 31 | 32 | /// Message view behind status bar 33 | public static let statusBar = SafeZoneConflicts(rawValue: 1 << 0) 34 | 35 | /// Message view behind the sensor notch on iPhone X 36 | public static let sensorNotch = SafeZoneConflicts(rawValue: 1 << 1) 37 | 38 | /// Message view behind home indicator on iPhone X 39 | public static let homeIndicator = SafeZoneConflicts(rawValue: 1 << 2) 40 | 41 | /// Message view is over the status bar on an iPhone 8 or lower. This is a special 42 | /// case because we logically expect the top safe area to be zero, but it is reported as 20 43 | /// (which seems like an iOS bug). We use the `overStatusBar` to indicate this special case. 44 | public static let overStatusBar = SafeZoneConflicts(rawValue: 1 << 3) 45 | } 46 | 47 | public class AnimationContext { 48 | 49 | public let messageView: UIView 50 | public let containerView: UIView 51 | public let safeZoneConflicts: SafeZoneConflicts 52 | public let interactiveHide: Bool 53 | 54 | init(messageView: UIView, containerView: UIView, safeZoneConflicts: SafeZoneConflicts, interactiveHide: Bool) { 55 | self.messageView = messageView 56 | self.containerView = containerView 57 | self.safeZoneConflicts = safeZoneConflicts 58 | self.interactiveHide = interactiveHide 59 | } 60 | } 61 | 62 | @MainActor 63 | public protocol Animator: AnyObject { 64 | 65 | /// Adopting classes should declare as `weak`. 66 | var delegate: AnimationDelegate? { get set } 67 | 68 | func show(context: AnimationContext, completion: @escaping AnimationCompletion) 69 | 70 | func hide(context: AnimationContext, completion: @escaping AnimationCompletion) 71 | 72 | /// The show animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`, 73 | /// then provide an estimate. This value is used by `SwiftMessagesSegue`. 74 | var showDuration: TimeInterval { get } 75 | 76 | /// The hide animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`, 77 | /// then provide an estimate. This value is used by `SwiftMessagesSegue`. 78 | var hideDuration: TimeInterval { get } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /SwiftMessages/BackgroundViewable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundViewable.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/15/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Message views that implement the `BackgroundViewable` protocol will have the 13 | pan-to-hide gesture recognizer installed in the `backgroundView`. Message views 14 | always span the full width of the containing view. Typically, the `backgroundView` 15 | property defines the message view's visible region, allowing for card-style views 16 | where the message view background is transparent and the background view is inset 17 | from by some amount. See CardView.nib, for example. 18 | 19 | This protocol is optional. Message views that don't implement `BackgroundViewable` 20 | will have the pan-to-hide gesture installed in the message view itself. 21 | */ 22 | public protocol BackgroundViewable { 23 | var backgroundView: UIView! { get } 24 | } -------------------------------------------------------------------------------- /SwiftMessages/CALayer+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/3/18. 6 | // Copyright © 2018 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | 11 | extension CALayer { 12 | func findAnimation(forKeyPath keyPath: String) -> CABasicAnimation? { 13 | return animationKeys()? 14 | .compactMap({ animation(forKey: $0) as? CABasicAnimation }) 15 | .filter({ $0.keyPath == keyPath }) 16 | .first 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SwiftMessages/CornerRoundingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRoundingView.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 7/28/18. 6 | // Copyright © 2018 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A background view that messages can use for rounding all or a subset of corners with squircles 12 | /// (the smoother method of rounding corners that you see on app icons). 13 | open class CornerRoundingView: UIView { 14 | 15 | /// Specifies the corner radius to use. 16 | @IBInspectable 17 | open var cornerRadius: CGFloat = 0 { 18 | didSet { 19 | updateMaskPath() 20 | } 21 | } 22 | 23 | /// Set to `true` for layouts where only the leading corners should be 24 | /// rounded. For example, the layout in TabView.xib rounds the bottom corners 25 | /// when displayed from the top and the top corners when displayed from the bottom. 26 | /// When this property is `true`, the `roundedCorners` property will be overwritten 27 | /// by relevant animators (e.g. `TopBottomAnimation`). 28 | @IBInspectable 29 | open var roundsLeadingCorners: Bool = false 30 | 31 | /// Specifies which corners should be rounded. When `roundsLeadingCorners = true`, relevant 32 | /// relevant animators (e.g. `TopBottomAnimation`) will overwrite the value of this property. 33 | open var roundedCorners: UIRectCorner = [.allCorners] { 34 | didSet { 35 | updateMaskPath() 36 | } 37 | } 38 | 39 | override public init(frame: CGRect) { 40 | super.init(frame: frame) 41 | sharedInit() 42 | } 43 | 44 | required public init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | sharedInit() 47 | } 48 | 49 | private func sharedInit() { 50 | layer.mask = shapeLayer 51 | } 52 | 53 | private let shapeLayer = CAShapeLayer() 54 | 55 | override open func layoutSubviews() { 56 | super.layoutSubviews() 57 | updateMaskPath() 58 | } 59 | 60 | private func updateMaskPath() { 61 | let newPath = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: roundedCorners, cornerRadii: cornerRadii).cgPath 62 | // Update the `shapeLayer's` path with animation if we detect our `layer's` size is being animated. 63 | // This is a workaround needed for smooth rotation animations. 64 | if let foundAnimation = layer.findAnimation(forKeyPath: "bounds.size") { 65 | // Update the `shapeLayer's` path with animation, copying the relevant properties 66 | // from the found animation. 67 | let animation = CABasicAnimation(keyPath: "path") 68 | animation.duration = foundAnimation.duration 69 | animation.timingFunction = foundAnimation.timingFunction 70 | animation.fromValue = shapeLayer.path 71 | animation.toValue = newPath 72 | shapeLayer.add(animation, forKey: "path") 73 | shapeLayer.path = newPath 74 | } else { 75 | // Update the `shapeLayer's` path without animation 76 | shapeLayer.path = newPath 77 | } 78 | } 79 | 80 | private var cornerRadii: CGSize { 81 | return CGSize(width: cornerRadius, height: cornerRadius) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SwiftMessages/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/7/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | The `SwiftMessagesError` enum contains the errors thrown by SwiftMessages. 13 | */ 14 | enum SwiftMessagesError: Error { 15 | case cannotLoadViewFromNib(nibName: String) 16 | case noRootViewController 17 | } 18 | -------------------------------------------------------------------------------- /SwiftMessages/HapticMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticMessage.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 1/23/24. 6 | // Copyright © 2024 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Message views that conform to `HapticMessage` can specify a haptic feedback to be used when presented. 13 | */ 14 | protocol HapticMessage { 15 | var defaultHaptic: SwiftMessages.Haptic? { get } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftMessages/Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Identifiable.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/1/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Message views that adopt the `Identifiable` protocol will have duplicate messages 13 | removed from the `MessageView` queue. Typically, the `id` would be set to a string 14 | representation of the content of the message view. For example, `MessageView`, combines 15 | the title and message body text. 16 | 17 | This protocol is optional. Message views that don't adopt `Identifiable` will not 18 | have duplicates removed. 19 | */ 20 | 21 | public protocol Identifiable { 22 | var id: String { get } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftMessages/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SwiftMessages/KeyboardTrackingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardTrackingView.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 5/20/19. 6 | // Copyright © 2019 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol KeyboardTrackingViewDelegate: AnyObject { 12 | func keyboardTrackingViewWillChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any]) 13 | func keyboardTrackingViewDidChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any]) 14 | } 15 | 16 | /// A view that adjusts it's height based on keyboard hide and show notifications. 17 | /// Pin it to the bottom of the screen using Auto Layout and then pin views that 18 | /// should avoid the keyboard to the top of it. Supply an instance of this class 19 | /// on `SwiftMessages.Config.keyboardTrackingView` or `SwiftMessagesSegue.keyboardTrackingView` 20 | /// for automatic keyboard avoidance for the entire SwiftMessages view or view controller. 21 | open class KeyboardTrackingView: UIView { 22 | 23 | public enum Change { 24 | case show 25 | case hide 26 | case frame 27 | } 28 | 29 | public weak var delegate: KeyboardTrackingViewDelegate? 30 | 31 | /// Typically, when a view controller is not being displayed, keyboard 32 | /// tracking should be paused to avoid responding to keyboard events 33 | /// caused by other view controllers or apps. Setting `isPaused = false` in 34 | /// `viewWillAppear` and `isPaused = true` in `viewWillDisappear` usually works. This class 35 | /// automatically pauses and resumes when the app resigns and becomes active, respectively. 36 | open var isPaused = false { 37 | didSet { 38 | if !isPaused { 39 | isAutomaticallyPaused = false 40 | } 41 | } 42 | } 43 | 44 | /// The margin to maintain between the keyboard and the top of the view. 45 | @IBInspectable open var topMargin: CGFloat = 0 46 | 47 | /// Subclasses can override this to do something before the change. 48 | open func willChange( 49 | change: KeyboardTrackingView.Change, 50 | userInfo: [AnyHashable : Any] 51 | ) {} 52 | 53 | /// Subclasses can override this to do something after the change. 54 | open func didChange( 55 | change: KeyboardTrackingView.Change, 56 | userInfo: [AnyHashable : Any] 57 | ) {} 58 | 59 | override public init(frame: CGRect) { 60 | super.init(frame: frame) 61 | postInit() 62 | } 63 | 64 | required public init?(coder aDecoder: NSCoder) { 65 | super.init(coder: aDecoder) 66 | } 67 | 68 | open override func awakeFromNib() { 69 | super.awakeFromNib() 70 | postInit() 71 | } 72 | 73 | private var isAutomaticallyPaused = false 74 | private var heightConstraint: NSLayoutConstraint! 75 | private var lastObservedKeyboardRect: CGRect? 76 | 77 | private func postInit() { 78 | translatesAutoresizingMaskIntoConstraints = false 79 | heightConstraint = heightAnchor.constraint(equalToConstant: 0) 80 | heightConstraint.isActive = true 81 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) 82 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 83 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) 84 | NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil) 85 | NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.didBecomeActiveNotification, object: nil) 86 | backgroundColor = .clear 87 | } 88 | 89 | @objc private func keyboardWillChangeFrame(_ notification: Notification) { 90 | show(change: .frame, notification) 91 | } 92 | 93 | @objc private func keyboardWillShow(_ notification: Notification) { 94 | show(change: .show, notification) 95 | } 96 | 97 | @objc private func keyboardWillHide(_ notification: Notification) { 98 | guard !(isPaused || isAutomaticallyPaused), 99 | let userInfo = (notification as NSNotification).userInfo else { return } 100 | guard heightConstraint.constant != 0 else { return } 101 | delegate?.keyboardTrackingViewWillChange(change: .hide, userInfo: userInfo) 102 | animateKeyboardChange(change: .hide, height: 0, userInfo: userInfo) 103 | } 104 | 105 | @objc private func pause() { 106 | isAutomaticallyPaused = true 107 | } 108 | 109 | @objc private func resume() { 110 | isAutomaticallyPaused = false 111 | } 112 | 113 | open override func layoutSubviews() { 114 | super.layoutSubviews() 115 | heightConstraint.constant = calculateHeightConstant() 116 | } 117 | 118 | private func show(change: Change, _ notification: Notification) { 119 | guard !(isPaused || isAutomaticallyPaused), 120 | let userInfo = (notification as NSNotification).userInfo, 121 | let value = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } 122 | willChange(change: change, userInfo: userInfo) 123 | delegate?.keyboardTrackingViewWillChange(change: change, userInfo: userInfo) 124 | lastObservedKeyboardRect = value.cgRectValue 125 | let newHeight = calculateHeightConstant() 126 | guard heightConstraint.constant != newHeight else { return } 127 | animateKeyboardChange(change: change, height: newHeight, userInfo: userInfo) 128 | } 129 | 130 | private func animateKeyboardChange(change: Change, height: CGFloat, userInfo: [AnyHashable: Any]) { 131 | if let durationNumber = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber { 132 | UIView.animate(withDuration: durationNumber.doubleValue, delay: 0, options: .curveEaseInOut, animations: { 133 | self.heightConstraint.constant = height 134 | self.updateConstraintsIfNeeded() 135 | self.superview?.layoutIfNeeded() 136 | }) { completed in 137 | self.didChange(change: change, userInfo: userInfo) 138 | self.delegate?.keyboardTrackingViewDidChange(change: change, userInfo: userInfo) 139 | } 140 | } 141 | } 142 | 143 | private func calculateHeightConstant() -> CGFloat { 144 | guard let keyboardRect = lastObservedKeyboardRect else { return 0 } 145 | let thisRect = convert(bounds, to: nil) 146 | return max(0, thisRect.maxY - keyboardRect.minY) + topMargin 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SwiftMessages/MarginAdjustable+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarginAdjustable+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 11/5/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension MarginAdjustable where Self: UIView { 12 | public func defaultMarginAdjustment(context: AnimationContext) -> UIEdgeInsets { 13 | var layoutMargins: UIEdgeInsets = layoutMarginAdditions 14 | var safeAreaInsets: UIEdgeInsets = { 15 | guard respectSafeArea else { return .zero } 16 | insetsLayoutMarginsFromSafeArea = false 17 | return self.safeAreaInsets 18 | }() 19 | if !context.safeZoneConflicts.isDisjoint(with: .overStatusBar) { 20 | safeAreaInsets.top = 0 21 | } 22 | layoutMargins = collapseLayoutMarginAdditions 23 | ? layoutMargins.collapse(toInsets: safeAreaInsets) 24 | : layoutMargins + safeAreaInsets 25 | return layoutMargins 26 | } 27 | } 28 | 29 | extension UIEdgeInsets { 30 | func collapse(toInsets insets: UIEdgeInsets) -> UIEdgeInsets { 31 | let top = self.top.collapse(toInset: insets.top) 32 | let left = self.left.collapse(toInset: insets.left) 33 | let bottom = self.bottom.collapse(toInset: insets.bottom) 34 | let right = self.right.collapse(toInset: insets.right) 35 | return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 36 | } 37 | } 38 | 39 | extension CGFloat { 40 | func collapse(toInset inset: CGFloat) -> CGFloat { 41 | return Swift.max(self, inset) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwiftMessages/MarginAdjustable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarginAdjustable.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/5/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /* 12 | Message views that implement the `MarginAdjustable` protocol will have their 13 | `layoutMargins` adjusted by SwiftMessages to account for the height of the 14 | status bar (when displayed under the status bar) and a small amount of 15 | overshoot in the bounce animation. `MessageView` implements this protocol 16 | by way of its parent class `BaseView`. 17 | 18 | For the effect of this protocol to work, subviews should be pinned to the 19 | message view's margins and the `layoutMargins` property should not be modified. 20 | 21 | This protocol is optional. A message view that doesn't implement `MarginAdjustable` 22 | is responsible for setting is own internal margins appropriately. 23 | */ 24 | public protocol MarginAdjustable { 25 | 26 | /// The amount to add to the safe area insets in calculating 27 | /// the layout margins. 28 | var layoutMarginAdditions: UIEdgeInsets { get } 29 | 30 | /// When `true`, SwiftMessages automatically collapses layout margin additions (topLayoutMarginAddition, etc.) 31 | /// when the default layout margins are greater than zero. This is typically used when a margin addition is only 32 | /// needed when the safe area inset is zero for a given edge. When the safe area inset for a given edge is non-zero, 33 | /// the additional margin is not added. 34 | var collapseLayoutMarginAdditions: Bool { get set } 35 | 36 | 37 | /// Start margins from the safe area. 38 | var respectSafeArea: Bool { get set } 39 | 40 | var bounceAnimationOffset: CGFloat { get set } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /SwiftMessages/MaskingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskingView.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 3/11/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class MaskingView: PassthroughView { 13 | 14 | func install(keyboardTrackingView: KeyboardTrackingView) { 15 | self.keyboardTrackingView = keyboardTrackingView 16 | keyboardTrackingView.translatesAutoresizingMaskIntoConstraints = false 17 | addSubview(keyboardTrackingView) 18 | keyboardTrackingView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 19 | keyboardTrackingView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 20 | keyboardTrackingView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 21 | } 22 | 23 | var accessibleElements: [NSObject] = [] 24 | 25 | weak var backgroundView: UIView? { 26 | didSet { 27 | oldValue?.removeFromSuperview() 28 | if let view = backgroundView { 29 | view.isUserInteractionEnabled = false 30 | view.frame = bounds 31 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 32 | addSubview(view) 33 | sendSubviewToBack(view) 34 | } 35 | } 36 | } 37 | 38 | override func accessibilityElementCount() -> Int { 39 | return accessibleElements.count 40 | } 41 | 42 | override func accessibilityElement(at index: Int) -> Any? { 43 | return accessibleElements[index] 44 | } 45 | 46 | override func index(ofAccessibilityElement element: Any) -> Int { 47 | guard let object = element as? NSObject else { return 0 } 48 | return accessibleElements.firstIndex(of: object) ?? 0 49 | } 50 | 51 | init() { 52 | super.init(frame: CGRect.zero) 53 | clipsToBounds = true 54 | } 55 | 56 | required init?(coder aDecoder: NSCoder) { 57 | super.init(coder: aDecoder) 58 | clipsToBounds = true 59 | } 60 | 61 | private var keyboardTrackingView: KeyboardTrackingView? 62 | 63 | override func addSubview(_ view: UIView) { 64 | super.addSubview(view) 65 | guard let keyboardTrackingView = keyboardTrackingView, 66 | view != keyboardTrackingView, 67 | view != backgroundView else { return } 68 | let offset: CGFloat 69 | if let adjustable = view as? MarginAdjustable { 70 | offset = -adjustable.bounceAnimationOffset 71 | } else { 72 | offset = 0 73 | } 74 | keyboardTrackingView.topAnchor.constraint( 75 | greaterThanOrEqualTo: view.bottomAnchor, 76 | constant: offset 77 | ).with(priority: UILayoutPriority(250)).isActive = true 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /SwiftMessages/MessageGeometryProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageGeometryProxy.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/24/24. 6 | // Copyright © 2024 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A data type that mimicks `GeomtryProxy` and is used with `swiftMessage()` modifier when the geomtry metrics of the container view 12 | /// are needed, particularly because `GeometryReader` doesn't work inside the view builder due to the way the message view is being 13 | /// displayed from UIKit. 14 | public struct MessageGeometryProxy { 15 | public var size: CGSize 16 | public var safeAreaInsets: EdgeInsets 17 | } 18 | -------------------------------------------------------------------------------- /SwiftMessages/MessageHostingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageHostingView.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 10/5/23. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | /// A rudimentary hosting view for SwiftUI messages. 12 | @available(iOS 14.0, *) 13 | public class MessageHostingView: UIView, Identifiable where Content: View { 14 | 15 | // MARK: - API 16 | 17 | public let id: String 18 | 19 | public init(id: String, content: Content) { 20 | self.id = id 21 | self.content = { _ in content } 22 | super.init(frame: .zero) 23 | backgroundColor = .clear 24 | } 25 | 26 | public init( 27 | message: Message, 28 | @ViewBuilder content: @escaping (Message, MessageGeometryProxy) -> Content 29 | ) where Message: Identifiable { 30 | self.id = message.id 31 | self.content = { geom in content(message, geom) } 32 | super.init(frame: .zero) 33 | backgroundColor = .clear 34 | } 35 | 36 | convenience public init(message: Message) where Message: MessageViewConvertible, Message.Content == Content { 37 | self.init(id: message.id, content: message.asMessageView() ) 38 | } 39 | 40 | // MARK: - Constants 41 | 42 | // MARK: - Variables 43 | 44 | private var hostVC: UIHostingController? 45 | private let content: (MessageGeometryProxy) -> Content 46 | 47 | // MARK: - Lifecycle 48 | 49 | @available(*, unavailable) 50 | required init?(coder _: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 55 | guard let view = super.hitTest(point, with: event) else { return nil } 56 | // Touches should pass through unless they land on a view that is rendering a SwiftUI element. 57 | if view == self { return nil } 58 | // In iOS 18 beta, the hit testing behavior changed in a weird way: when a SwiftUI element is tapped, 59 | // the first hit test returns the view that renders the SwiftUI element. However, a second identical hit 60 | // test is performed(!) and on the second test, the `UIHostingController`'s view is returned. We want touches 61 | // to pass through that view. In iOS 17, we would just return `nil` in that case. However, in iOS 18, the 62 | // second hit test is actuall essential to touches being delivered to the SwiftUI elements. The new approach 63 | // is to iterate overall all of the subviews, which are all presumably rendering SwiftUI elements, and 64 | // only return `nil` if the point is not inside any of these subviews. 65 | if view.superview == self { 66 | for subview in view.subviews { 67 | let subviewPoint = self.convert(point, to: subview) 68 | if subview.point(inside: subviewPoint, with: event) { 69 | return view 70 | } 71 | } 72 | return nil 73 | } 74 | return view 75 | } 76 | 77 | public override func didMoveToSuperview() { 78 | guard let superview = self.superview else { return } 79 | let size = superview.bounds.size 80 | let insets = superview.safeAreaInsets 81 | let ltr = superview.effectiveUserInterfaceLayoutDirection == .leftToRight 82 | let proxy = MessageGeometryProxy( 83 | size: CGSize( 84 | width: size.width - insets.left - insets.right, 85 | height: size.height - insets.top - insets.bottom 86 | ), 87 | safeAreaInsets: EdgeInsets( 88 | top: insets.top, 89 | leading: ltr ? insets.left : insets.right, 90 | bottom: insets.bottom, 91 | trailing: ltr ? insets.right : insets.left 92 | ) 93 | ) 94 | let hostVC = UIHostingController(rootView: content(proxy)) 95 | self.hostVC = hostVC 96 | hostVC.loadViewIfNeeded() 97 | installContentView(hostVC.view) 98 | hostVC.view.backgroundColor = .clear 99 | 100 | } 101 | 102 | // MARK: - Configuration 103 | 104 | private func installContentView(_ contentView: UIView) { 105 | contentView.translatesAutoresizingMaskIntoConstraints = false 106 | addSubview(contentView) 107 | NSLayoutConstraint.activate([ 108 | contentView.topAnchor.constraint(equalTo: topAnchor), 109 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor), 110 | contentView.leftAnchor.constraint(equalTo: leftAnchor), 111 | contentView.rightAnchor.constraint(equalTo: rightAnchor), 112 | ]) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /SwiftMessages/MessageViewConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageViewConvertible.swift 3 | // SwiftUIDemo 4 | // 5 | // Created by Timothy Moose on 10/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 14.0, *) 11 | /// A protocol used to display a SwiftUI message view using the `swiftMessage()` modifier. 12 | public protocol MessageViewConvertible: Equatable, Identifiable { 13 | associatedtype Content: View 14 | func asMessageView() -> Content 15 | } 16 | 17 | -------------------------------------------------------------------------------- /SwiftMessages/NSBundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/8/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private class BundleToken {} 12 | 13 | extension Bundle { 14 | // This is copied method from SPM generated Bundle.module for CocoaPods support 15 | static func sm_frameworkBundle() -> Bundle { 16 | 17 | let candidates = [ 18 | // Bundle should be present here when the package is linked into an App. 19 | Bundle.main.resourceURL, 20 | 21 | // Bundle should be present here when the package is linked into a framework. 22 | Bundle(for: BundleToken.self).resourceURL, 23 | 24 | // For command-line tools. 25 | Bundle.main.bundleURL, 26 | ] 27 | 28 | let bundleNames = [ 29 | // For Swift Package Manager 30 | "SwiftMessages_SwiftMessages", 31 | 32 | // For Carthage 33 | "SwiftMessages", 34 | ] 35 | 36 | for bundleName in bundleNames { 37 | for candidate in candidates { 38 | let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") 39 | if let bundle = bundlePath.flatMap(Bundle.init(url:)) { 40 | return bundle 41 | } 42 | } 43 | } 44 | 45 | // Return whatever bundle this code is in as a last resort. 46 | return Bundle(for: BundleToken.self) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SwiftMessages/NSLayoutConstraint+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraint+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 5/18/19. 6 | // Copyright © 2019 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSLayoutConstraint { 12 | func with(priority: UILayoutPriority) -> NSLayoutConstraint { 13 | self.priority = priority 14 | return self 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftMessages/PassthroughView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughView.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/5/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PassthroughView: UIControl { 12 | 13 | var tappedHander: (() -> Void)? 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | initCommon() 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | super.init(coder: aDecoder) 22 | initCommon() 23 | } 24 | 25 | private func initCommon() { 26 | addTarget(self, action: #selector(tapped), for: .touchUpInside) 27 | } 28 | 29 | @objc func tapped() { 30 | tappedHander?() 31 | } 32 | 33 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 34 | let view = super.hitTest(point, with: event) 35 | return view == self && tappedHander == nil ? nil : view 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftMessages/PassthroughWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughWindow.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/5/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PassthroughWindow: UIWindow { 12 | 13 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 14 | // iOS has started embedding the SwiftMessages view in private views that block 15 | // interaction with views underneath, essentially making the window behave like a modal. 16 | // To work around this, we'll ignore hit test results on these views. 17 | let view = super.hitTest(point, with: event) 18 | if let view = view, 19 | let hitTestView = hitTestView, 20 | hitTestView.isDescendant(of: view) && hitTestView != view { 21 | return nil 22 | } 23 | return view 24 | } 25 | 26 | init(hitTestView: UIView) { 27 | self.hitTestView = hitTestView 28 | super.init(frame: .zero) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | private weak var hitTestView: UIView? 36 | } 37 | -------------------------------------------------------------------------------- /SwiftMessages/PhysicsAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhysicsAnimation.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/14/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @MainActor 12 | public class PhysicsAnimation: NSObject, Animator { 13 | 14 | public enum Placement { 15 | case top 16 | case center 17 | case bottom 18 | } 19 | 20 | open var placement: Placement = .center 21 | 22 | open var showDuration: TimeInterval = 0.5 23 | 24 | open var hideDuration: TimeInterval = 0.15 25 | 26 | public var panHandler = PhysicsPanHandler() 27 | 28 | public weak var delegate: AnimationDelegate? 29 | weak var messageView: UIView? 30 | weak var containerView: UIView? 31 | var context: AnimationContext? 32 | 33 | public override init() {} 34 | 35 | init(delegate: AnimationDelegate) { 36 | self.delegate = delegate 37 | } 38 | 39 | public func show(context: AnimationContext, completion: @escaping AnimationCompletion) { 40 | NotificationCenter.default.addObserver( 41 | self, 42 | selector: #selector(adjustMargins), 43 | name: UIDevice.orientationDidChangeNotification, 44 | object: nil 45 | ) 46 | install(context: context) 47 | showAnimation(context: context, completion: completion) 48 | } 49 | 50 | public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) { 51 | NotificationCenter.default.removeObserver(self) 52 | if panHandler.isOffScreen { 53 | context.messageView.alpha = 0 54 | panHandler.state?.stop() 55 | } 56 | let view = context.messageView 57 | self.context = context 58 | CATransaction.begin() 59 | CATransaction.setCompletionBlock { 60 | view.alpha = 1 61 | view.transform = CGAffineTransform.identity 62 | completion(true) 63 | } 64 | UIView.animate( 65 | withDuration: hideDuration, 66 | delay: 0, 67 | options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], 68 | animations: { 69 | view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) 70 | }, 71 | completion: nil 72 | ) 73 | UIView.animate( 74 | withDuration: hideDuration, 75 | delay: 0, 76 | options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], 77 | animations: { 78 | view.alpha = 0 79 | }, 80 | completion: nil 81 | ) 82 | CATransaction.commit() 83 | } 84 | 85 | func install(context: AnimationContext) { 86 | let view = context.messageView 87 | let container = context.containerView 88 | messageView = view 89 | containerView = container 90 | self.context = context 91 | view.translatesAutoresizingMaskIntoConstraints = false 92 | container.addSubview(view) 93 | switch placement { 94 | case .center: 95 | view.centerYAnchor.constraint( 96 | equalTo: container.centerYAnchor 97 | ) 98 | .with(priority: UILayoutPriority(200)) 99 | .isActive = true 100 | case .top: 101 | view.topAnchor.constraint( 102 | equalTo: container.topAnchor 103 | ) 104 | .with(priority: UILayoutPriority(200)) 105 | .isActive = true 106 | case .bottom: 107 | view.bottomAnchor.constraint( 108 | equalTo: container.bottomAnchor 109 | ) 110 | .with(priority: UILayoutPriority(200)) 111 | .isActive = true 112 | } 113 | NSLayoutConstraint.activate([ 114 | view.leadingAnchor.constraint(equalTo: container.leadingAnchor), 115 | view.trailingAnchor.constraint(equalTo: container.trailingAnchor), 116 | // Don't allow the message to spill outside of the top or bottom of the container. 117 | view.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor), 118 | view.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor), 119 | ]) 120 | // Important to layout now in order to get the right safe area insets 121 | container.layoutIfNeeded() 122 | adjustMargins() 123 | container.layoutIfNeeded() 124 | installInteractive(context: context) 125 | } 126 | 127 | @objc public func adjustMargins() { 128 | guard let adjustable = messageView as? MarginAdjustable & UIView, 129 | let context = context else { return } 130 | adjustable.preservesSuperviewLayoutMargins = false 131 | adjustable.insetsLayoutMarginsFromSafeArea = false 132 | adjustable.layoutMargins = adjustable.defaultMarginAdjustment(context: context) 133 | } 134 | 135 | func showAnimation(context: AnimationContext, completion: @escaping AnimationCompletion) { 136 | let view = context.messageView 137 | view.alpha = 0.25 138 | view.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) 139 | CATransaction.begin() 140 | CATransaction.setCompletionBlock { 141 | completion(true) 142 | } 143 | UIView.animate(withDuration: showDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: { 144 | view.transform = CGAffineTransform.identity 145 | }, completion: nil) 146 | UIView.animate(withDuration: 0.3 * showDuration, delay: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: { 147 | view.alpha = 1 148 | }, completion: nil) 149 | CATransaction.commit() 150 | } 151 | 152 | func installInteractive(context: AnimationContext) { 153 | guard context.interactiveHide else { return } 154 | panHandler.configure(context: context, animator: self) 155 | } 156 | } 157 | 158 | 159 | -------------------------------------------------------------------------------- /SwiftMessages/PhysicsPanHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhysicsPanHandler.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/25/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @MainActor 12 | open class PhysicsPanHandler { 13 | 14 | public var hideDelay: TimeInterval = 0.2 15 | 16 | public struct MotionSnapshot { 17 | var angle: CGFloat 18 | var time: CFAbsoluteTime 19 | } 20 | 21 | @MainActor 22 | public final class State { 23 | 24 | weak var messageView: UIView? 25 | weak var containerView: UIView? 26 | var dynamicAnimator: UIDynamicAnimator 27 | var itemBehavior: UIDynamicItemBehavior 28 | var attachmentBehavior: UIAttachmentBehavior? { 29 | didSet { 30 | if let oldValue = oldValue { 31 | dynamicAnimator.removeBehavior(oldValue) 32 | } 33 | if let attachmentBehavior = attachmentBehavior { 34 | dynamicAnimator.addBehavior(attachmentBehavior) 35 | addSnapshot() 36 | } 37 | } 38 | } 39 | var snapshots: [MotionSnapshot] = [] 40 | 41 | public init(messageView: UIView, containerView: UIView) { 42 | self.messageView = messageView 43 | self.containerView = containerView 44 | let dynamicAnimator = UIDynamicAnimator(referenceView: containerView) 45 | let itemBehavior = UIDynamicItemBehavior(items: [messageView]) 46 | itemBehavior.allowsRotation = true 47 | dynamicAnimator.addBehavior(itemBehavior) 48 | self.itemBehavior = itemBehavior 49 | self.dynamicAnimator = dynamicAnimator 50 | } 51 | 52 | func update(attachmentAnchorPoint anchorPoint: CGPoint) { 53 | addSnapshot() 54 | attachmentBehavior?.anchorPoint = anchorPoint 55 | } 56 | 57 | func addSnapshot() { 58 | let angle = messageView?.angle ?? snapshots.last?.angle ?? 0 59 | let time = CFAbsoluteTimeGetCurrent() 60 | snapshots.append(MotionSnapshot(angle: angle, time: time)) 61 | } 62 | 63 | public func stop() { 64 | guard let messageView = messageView else { 65 | dynamicAnimator.removeAllBehaviors() 66 | return 67 | } 68 | let center = messageView.center 69 | let transform = messageView.transform 70 | dynamicAnimator.removeAllBehaviors() 71 | messageView.center = center 72 | messageView.transform = transform 73 | } 74 | 75 | public var angularVelocity: CGFloat { 76 | guard let last = snapshots.last else { return 0 } 77 | for previous in snapshots.reversed() { 78 | // Ignore snapshots where the angle or time hasn't changed to avoid degenerate cases. 79 | if previous.angle != last.angle && previous.time != last.time { 80 | return (last.angle - previous.angle) / CGFloat(last.time - previous.time) 81 | } 82 | } 83 | return 0 84 | } 85 | } 86 | 87 | weak var animator: Animator? 88 | weak var messageView: UIView? 89 | weak var containerView: UIView? 90 | private(set) public var state: State? 91 | private(set) public var isOffScreen = false 92 | private var restingCenter: CGPoint? 93 | 94 | public init() {} 95 | 96 | public private(set) lazy var pan: UIPanGestureRecognizer = { 97 | let pan = UIPanGestureRecognizer() 98 | pan.addTarget(self, action: #selector(pan(_:))) 99 | return pan 100 | }() 101 | 102 | public func configure(context: AnimationContext, animator: Animator) { 103 | if let oldView = (messageView as? BackgroundViewable)?.backgroundView ?? messageView { 104 | oldView.removeGestureRecognizer(pan) 105 | } 106 | messageView = context.messageView 107 | let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView 108 | view?.addGestureRecognizer(pan) 109 | containerView = context.containerView 110 | self.animator = animator 111 | } 112 | 113 | @objc func pan(_ pan: UIPanGestureRecognizer) { 114 | guard let messageView = messageView, let containerView = containerView, let animator = animator else { return } 115 | let anchorPoint = pan.location(in: containerView) 116 | switch pan.state { 117 | case .began: 118 | animator.delegate?.panStarted(animator: animator) 119 | let state = State(messageView: messageView, containerView: containerView) 120 | self.state = state 121 | let center = messageView.center 122 | restingCenter = center 123 | let offset = UIOffset(horizontal: anchorPoint.x - center.x, vertical: anchorPoint.y - center.y) 124 | let attachmentBehavior = UIAttachmentBehavior(item: messageView, offsetFromCenter: offset, attachedToAnchor: anchorPoint) 125 | state.attachmentBehavior = attachmentBehavior 126 | state.itemBehavior.action = { [weak self, weak messageView, weak containerView] in 127 | guard let self = self, !self.isOffScreen, let messageView = messageView, let containerView = containerView, let animator = self.animator else { return } 128 | let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView 129 | let frame = containerView.convert(view.bounds, from: view) 130 | if !containerView.bounds.intersects(frame) { 131 | self.isOffScreen = true 132 | Task { 133 | try? await Task.sleep(seconds: self.hideDelay) 134 | animator.delegate?.hide(animator: animator) 135 | } 136 | } 137 | } 138 | case .changed: 139 | guard let state = state else { return } 140 | state.update(attachmentAnchorPoint: anchorPoint) 141 | case .ended, .cancelled: 142 | guard let state = state else { return } 143 | state.update(attachmentAnchorPoint: anchorPoint) 144 | let velocity = pan.velocity(in: containerView) 145 | let angularVelocity = state.angularVelocity 146 | let speed = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2)) 147 | // The multiplier on angular velocity was determined by hand-tuning 148 | let energy = sqrt(pow(speed, 2) + pow(angularVelocity * 75, 2)) 149 | if energy > 200 && speed > 600 { 150 | // Limit the speed and angular velocity to reasonable values 151 | let speedScale = speed > 0 ? min(1, 1800 / speed) : 1 152 | let escapeVelocity = CGPoint(x: velocity.x * speedScale, y: velocity.y * speedScale) 153 | let angularSpeedScale = min(1, 10 / abs(angularVelocity)) 154 | let escapeAngularVelocity = angularVelocity * angularSpeedScale 155 | state.itemBehavior.addLinearVelocity(escapeVelocity, for: messageView) 156 | state.itemBehavior.addAngularVelocity(escapeAngularVelocity, for: messageView) 157 | state.attachmentBehavior = nil 158 | } else { 159 | state.stop() 160 | self.state = nil 161 | animator.delegate?.panEnded(animator: animator) 162 | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0, options: .beginFromCurrentState, animations: { 163 | messageView.center = self.restingCenter ?? CGPoint(x: containerView.bounds.width / 2, y: containerView.bounds.height / 2) 164 | messageView.transform = CGAffineTransform.identity 165 | }, completion: nil) 166 | } 167 | default: 168 | break 169 | } 170 | } 171 | } 172 | 173 | extension UIView { 174 | var angle: CGFloat { 175 | // http://stackoverflow.com/a/2051861/1271826 176 | return atan2(transform.b, transform.a) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /SwiftMessages/Resources/StatusLine.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIcon.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIcon@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIcon@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconLight.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconLight@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconLight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconLight@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconSubtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconSubtle.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconSubtle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconSubtle@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/errorIconSubtle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/errorIconSubtle@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIcon.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIcon@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIcon@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconLight.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconLight@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconLight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconLight@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconSubtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconSubtle.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconSubtle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconSubtle@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/infoIconSubtle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/infoIconSubtle@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIcon.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIcon@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIcon@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconLight.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconLight@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconLight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconLight@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconSubtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconSubtle.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconSubtle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconSubtle@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/successIconSubtle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/successIconSubtle@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIcon.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIcon@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIcon@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconLight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconLight.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconLight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconLight@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconLight@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconLight@3x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconSubtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconSubtle.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconSubtle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconSubtle@2x.png -------------------------------------------------------------------------------- /SwiftMessages/Resources/warningIconSubtle@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftKickMobile/SwiftMessages/b929e04ae9bac912b8442716f13ccd99812460ff/SwiftMessages/Resources/warningIconSubtle@3x.png -------------------------------------------------------------------------------- /SwiftMessages/SwiftMessageModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMessageModifier.swift 3 | // SwiftUIDemo 4 | // 5 | // Created by Timothy Moose on 10/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 14.0, *) 11 | public extension View { 12 | /// A view modifier for displaying a message using similar semantics to the `.sheet()` modifier. 13 | func swiftMessage( 14 | message: Binding, 15 | config: SwiftMessages.Config? = nil, 16 | swiftMessages: SwiftMessages? = nil, 17 | @ViewBuilder messageContent: @escaping (Message) -> MessageContent 18 | ) -> some View where Message: Equatable & Identifiable, MessageContent: View { 19 | swiftMessage(message: message, config: config, swiftMessages: swiftMessages) { message, _ in 20 | messageContent(message) 21 | } 22 | } 23 | 24 | /// A view modifier for displaying a message using similar semantics to the `.sheet()` modifier. This variant provides a 25 | /// `SwiftMessageGeometryProxy`. The proxy is useful when one needs to know the geometry metrics of the container view, 26 | /// particularly because `GeometryReader` doesn't work inside the view builder due to the way the message view is being 27 | /// displayed from UIKit. 28 | func swiftMessage( 29 | message: Binding, 30 | config: SwiftMessages.Config? = nil, 31 | swiftMessages: SwiftMessages? = nil, 32 | @ViewBuilder messageContent: @escaping (Message, MessageGeometryProxy) -> MessageContent 33 | ) -> some View where Message: Equatable & Identifiable, MessageContent: View { 34 | modifier( 35 | SwiftMessageModifier( 36 | message: message, 37 | config: config, 38 | swiftMessages: swiftMessages, 39 | messageContent: messageContent 40 | ) 41 | ) 42 | } 43 | 44 | /// A state-based modifier for displaying a message when `Message` conforms to `MessageViewConvertible`. This variant should be used if the message 45 | /// 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. 46 | func swiftMessage( 47 | message: Binding, 48 | config: SwiftMessages.Config? = nil, 49 | swiftMessages: SwiftMessages? = nil 50 | ) -> some View where Message: MessageViewConvertible { 51 | swiftMessage(message: message, config: config, swiftMessages: swiftMessages) { content in 52 | content.asMessageView() 53 | } 54 | } 55 | } 56 | 57 | @available(iOS 14.0, *) 58 | private struct SwiftMessageModifier: ViewModifier where Message: Equatable & Identifiable, MessageContent: View { 59 | 60 | // MARK: - API 61 | 62 | fileprivate init( 63 | message: Binding, 64 | config: SwiftMessages.Config? = nil, 65 | swiftMessages: SwiftMessages? = nil, 66 | @ViewBuilder messageContent: @escaping (Message) -> MessageContent 67 | ) { 68 | _message = message 69 | self.config = config 70 | self.swiftMessages = swiftMessages 71 | self.messageContent = { message, _ in 72 | messageContent(message) 73 | } 74 | } 75 | 76 | fileprivate init( 77 | message: Binding, 78 | config: SwiftMessages.Config? = nil, 79 | swiftMessages: SwiftMessages? = nil, 80 | @ViewBuilder messageContent: @escaping (Message, MessageGeometryProxy) -> MessageContent 81 | ) { 82 | _message = message 83 | self.config = config 84 | self.swiftMessages = swiftMessages 85 | self.messageContent = messageContent 86 | } 87 | 88 | fileprivate init( 89 | message: Binding, 90 | config: SwiftMessages.Config? = nil, 91 | swiftMessages: SwiftMessages? = nil 92 | ) where Message: MessageViewConvertible, Message.Content == MessageContent { 93 | _message = message 94 | self.config = config 95 | self.swiftMessages = swiftMessages 96 | self.messageContent = { message, _ in 97 | message.asMessageView() 98 | } 99 | } 100 | 101 | // MARK: - Constants 102 | 103 | // MARK: - Variables 104 | 105 | @Binding private var message: Message? 106 | private let config: SwiftMessages.Config? 107 | private let swiftMessages: SwiftMessages? 108 | @ViewBuilder private let messageContent: (Message, MessageGeometryProxy) -> MessageContent 109 | 110 | // MARK: - Body 111 | 112 | func body(content: Content) -> some View { 113 | content 114 | .onChange(of: message) { message in 115 | let show: @MainActor (SwiftMessages.Config, UIView) -> Void = swiftMessages?.show(config:view:) ?? SwiftMessages.show(config:view:) 116 | let hideAll: @MainActor () -> Void = swiftMessages?.hideAll ?? SwiftMessages.hideAll 117 | switch message { 118 | case let message?: 119 | let view = MessageHostingView(message: message, content: messageContent) 120 | var config = config ?? swiftMessages?.defaultConfig ?? SwiftMessages.defaultConfig 121 | config.eventListeners.append { event in 122 | if case .didHide = event, event.id == self.message?.id { 123 | self.message = nil 124 | } 125 | } 126 | hideAll() 127 | show(config, view) 128 | case .none: 129 | hideAll() 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /SwiftMessages/SwiftMessages.Config+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMessages.Config+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 12/26/20. 6 | // Copyright © 2020 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension SwiftMessages.Config { 12 | var windowLevel: UIWindow.Level? { 13 | switch presentationContext { 14 | case .window(let level): return level 15 | case .windowScene(_, let level): return level 16 | default: return nil 17 | } 18 | } 19 | 20 | @available(iOS 13.0, *) 21 | var windowScene: UIWindowScene? { 22 | switch presentationContext { 23 | case .windowScene(let scene, _): return scene as? UIWindowScene 24 | default: 25 | #if SWIFTMESSAGES_APP_EXTENSIONS 26 | return nil 27 | #else 28 | return UIWindow.keyWindow?.windowScene 29 | #endif 30 | } 31 | } 32 | 33 | var shouldBecomeKeyWindow: Bool { 34 | if let becomeKeyWindow = becomeKeyWindow { return becomeKeyWindow } 35 | switch dimMode { 36 | case .gray, .color, .blur: 37 | // Should become key window in modal presentation style 38 | // for proper VoiceOver handling. 39 | return true 40 | case .none: 41 | return false 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SwiftMessages/SwiftMessages.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMessages.h 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/9/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SwiftMessages. 12 | FOUNDATION_EXPORT double SwiftMessagesVersionNumber; 13 | 14 | //! Project version string for SwiftMessages. 15 | FOUNDATION_EXPORT const unsigned char SwiftMessagesVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SwiftMessages/Task+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 12/3/23. 6 | // Copyright © 2023 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Task where Success == Never, Failure == Never { 12 | static func sleep(seconds: TimeInterval) async throws { 13 | try await sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftMessages/Theme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Theme.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/7/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The theme enum specifies the built-in theme options 12 | public enum Theme { 13 | case info 14 | case success 15 | case warning 16 | case error 17 | } 18 | 19 | /// The Icon enum provides type-safe access to the included icons. 20 | public enum Icon: String { 21 | 22 | case error = "errorIcon" 23 | case warning = "warningIcon" 24 | case success = "successIcon" 25 | case info = "infoIcon" 26 | case errorLight = "errorIconLight" 27 | case warningLight = "warningIconLight" 28 | case successLight = "successIconLight" 29 | case infoLight = "infoIconLight" 30 | case errorSubtle = "errorIconSubtle" 31 | case warningSubtle = "warningIconSubtle" 32 | case successSubtle = "successIconSubtle" 33 | case infoSubtle = "infoIconSubtle" 34 | 35 | /// Returns the associated image. 36 | public var image: UIImage { 37 | return UIImage(named: rawValue, in: Bundle.sm_frameworkBundle(), compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) 38 | } 39 | } 40 | 41 | /// The IconStyle enum specifies the different variations of the included icons. 42 | public enum IconStyle { 43 | 44 | case `default` 45 | case light 46 | case subtle 47 | case none 48 | 49 | /// Returns the image for the given theme 50 | public func image(theme: Theme) -> UIImage? { 51 | switch (theme, self) { 52 | case (.info, .default): return Icon.info.image 53 | case (.info, .light): return Icon.infoLight.image 54 | case (.info, .subtle): return Icon.infoSubtle.image 55 | case (.success, .default): return Icon.success.image 56 | case (.success, .light): return Icon.successLight.image 57 | case (.success, .subtle): return Icon.successSubtle.image 58 | case (.warning, .default): return Icon.warning.image 59 | case (.warning, .light): return Icon.warningLight.image 60 | case (.warning, .subtle): return Icon.warningSubtle.image 61 | case (.error, .default): return Icon.error.image 62 | case (.error, .light): return Icon.errorLight.image 63 | case (.error, .subtle): return Icon.errorSubtle.image 64 | default: return nil 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /SwiftMessages/TopBottomAnimationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopBottomAnimationStyle.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/23/24. 6 | // Copyright © 2024 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | public enum TopBottomAnimationStyle { 10 | case top 11 | case bottom 12 | } 13 | -------------------------------------------------------------------------------- /SwiftMessages/TopBottomPresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Julien Di Marco on 23/04/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - TopBottom Presentable Definition 11 | 12 | @MainActor 13 | protocol TopBottomPresentable { 14 | var topBottomStyle: TopBottomAnimationStyle? { get } 15 | } 16 | 17 | // MARK: - TopBottom Presentable Conformances 18 | 19 | extension TopBottomAnimation: TopBottomPresentable { 20 | var topBottomStyle: TopBottomAnimationStyle? { return style } 21 | } 22 | 23 | extension PhysicsAnimation: TopBottomPresentable { 24 | var topBottomStyle: TopBottomAnimationStyle? { 25 | switch placement { 26 | case .top: return .top 27 | case .bottom: return .bottom 28 | default: return nil 29 | } 30 | } 31 | } 32 | 33 | // MARK: - Presentation Style Convenience 34 | 35 | extension SwiftMessages.PresentationStyle { 36 | /// A temporary workaround to allow custom presentation contexts using `TopBottomAnimation` 37 | /// to display properly behind bars. THe long term solution is to refactor all of the 38 | /// presentation context logic to work with safe area insets. 39 | @MainActor 40 | var topBottomStyle: TopBottomAnimationStyle? { 41 | switch self { 42 | case .top: return .top 43 | case .bottom: return .bottom 44 | case .custom(let animator as TopBottomPresentable): return animator.topBottomStyle 45 | case .center: return nil 46 | default: return nil 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SwiftMessages/UIEdgeInsets+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsets+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 5/23/18. 6 | // Copyright © 2018 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIEdgeInsets { 12 | public static func +(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { 13 | let topSum = left.top + right.top 14 | let leftSum = left.left + right.left 15 | let bottomSum = left.bottom + right.bottom 16 | let rightSum = left.right + right.right 17 | return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum) 18 | } 19 | 20 | public static func -(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { 21 | let topSum = left.top - right.top 22 | let leftSum = left.left - right.left 23 | let bottomSum = left.bottom - right.bottom 24 | let rightSum = left.right - right.right 25 | return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SwiftMessages/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/5/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | func sm_selectPresentationContextTopDown(_ config: SwiftMessages.Config) -> UIViewController { 14 | let topBottomStyle = config.presentationStyle.topBottomStyle 15 | if let presented = presentedViewController { 16 | return presented.sm_selectPresentationContextTopDown(config) 17 | } else if case .top? = topBottomStyle, let navigationController = sm_selectNavigationControllerTopDown() { 18 | return navigationController 19 | } else if case .bottom? = topBottomStyle, let tabBarController = sm_selectTabBarControllerTopDown() { 20 | return tabBarController 21 | } 22 | return WindowViewController.newInstance(config: config) 23 | } 24 | 25 | fileprivate func sm_selectNavigationControllerTopDown() -> UINavigationController? { 26 | if let presented = presentedViewController { 27 | return presented.sm_selectNavigationControllerTopDown() 28 | } else if let navigationController = self as? UINavigationController { 29 | if navigationController.sm_isVisible(view: navigationController.navigationBar) { 30 | return navigationController 31 | } 32 | return navigationController.topViewController?.sm_selectNavigationControllerTopDown() 33 | } else if let tabBarController = self as? UITabBarController { 34 | return tabBarController.selectedViewController?.sm_selectNavigationControllerTopDown() 35 | } 36 | return nil 37 | } 38 | 39 | fileprivate func sm_selectTabBarControllerTopDown() -> UITabBarController? { 40 | if let presented = presentedViewController { 41 | return presented.sm_selectTabBarControllerTopDown() 42 | } else if let navigationController = self as? UINavigationController { 43 | return navigationController.topViewController?.sm_selectTabBarControllerTopDown() 44 | } else if let tabBarController = self as? UITabBarController { 45 | if tabBarController.sm_isVisible(view: tabBarController.tabBar) { 46 | return tabBarController 47 | } 48 | return tabBarController.selectedViewController?.sm_selectTabBarControllerTopDown() 49 | } 50 | return nil 51 | } 52 | 53 | func sm_selectPresentationContextBottomUp(_ config: SwiftMessages.Config) -> UIViewController { 54 | let topBottomStyle = config.presentationStyle.topBottomStyle 55 | if let parent = parent { 56 | if let navigationController = parent as? UINavigationController { 57 | if case .top? = topBottomStyle, navigationController.sm_isVisible(view: navigationController.navigationBar) { 58 | return navigationController 59 | } 60 | return navigationController.sm_selectPresentationContextBottomUp(config) 61 | } else if let tabBarController = parent as? UITabBarController { 62 | if case .bottom? = topBottomStyle, tabBarController.sm_isVisible(view: tabBarController.tabBar) { 63 | return tabBarController 64 | } 65 | return tabBarController.sm_selectPresentationContextBottomUp(config) 66 | } 67 | } 68 | if self.view is UITableView { 69 | // Never select scroll view as presentation context 70 | // because, you know, it scrolls. 71 | if let parent = self.parent { 72 | return parent.sm_selectPresentationContextBottomUp(config) 73 | } else { 74 | return WindowViewController.newInstance(config: config) 75 | } 76 | } 77 | return self 78 | } 79 | 80 | func sm_isVisible(view: UIView) -> Bool { 81 | if view.isHidden { return false } 82 | if view.alpha == 0.0 { return false } 83 | let frame = self.view.convert(view.bounds, from: view) 84 | if !self.view.bounds.intersects(frame) { return false } 85 | return true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SwiftMessages/UIWindow+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+Extensions.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 3/11/21. 6 | // Copyright © 2021 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIWindow { 12 | #if !SWIFTMESSAGES_APP_EXTENSIONS 13 | static var keyWindow: UIWindow? { 14 | return UIApplication.shared.connectedScenes 15 | .sorted { $0.activationState.sortPriority < $1.activationState.sortPriority } 16 | .compactMap { $0 as? UIWindowScene } 17 | .compactMap { $0.windows.first { $0.isKeyWindow } } 18 | .first 19 | } 20 | #endif 21 | } 22 | 23 | @available(iOS 13.0, *) 24 | private extension UIScene.ActivationState { 25 | var sortPriority: Int { 26 | switch self { 27 | case .foregroundActive: return 1 28 | case .foregroundInactive: return 2 29 | case .background: return 3 30 | case .unattached: return 4 31 | @unknown default: return 5 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SwiftMessages/Weak.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weak.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 6/4/17. 6 | // Copyright © 2017 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Weak { 12 | public weak var value : T? 13 | public init(value: T?) { 14 | self.value = value 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftMessages/WindowScene.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | /// A workaround for the change in Xcode 13 that prevents using `@availability` attribute 5 | /// with `enum` cases containing associated values. 6 | public protocol WindowScene {} 7 | 8 | @available(iOS 13.0, *) 9 | extension UIWindowScene: WindowScene {} 10 | -------------------------------------------------------------------------------- /SwiftMessages/WindowViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowViewController.swift 3 | // SwiftMessages 4 | // 5 | // Created by Timothy Moose on 8/1/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class WindowViewController: UIViewController 12 | { 13 | override open var shouldAutorotate: Bool { 14 | return config.shouldAutorotate 15 | } 16 | 17 | convenience public init() { 18 | self.init(config: SwiftMessages.Config()) 19 | } 20 | 21 | public init(config: SwiftMessages.Config) { 22 | self.config = config 23 | let view = PassthroughView() 24 | let window = PassthroughWindow(hitTestView: view) 25 | self.window = window 26 | super.init(nibName: nil, bundle: nil) 27 | self.view = view 28 | window.rootViewController = self 29 | window.windowLevel = config.windowLevel ?? UIWindow.Level.normal 30 | window.overrideUserInterfaceStyle = config.overrideUserInterfaceStyle 31 | } 32 | 33 | func install() { 34 | window?.windowScene = config.windowScene 35 | #if !SWIFTMESSAGES_APP_EXTENSIONS 36 | previousKeyWindow = UIWindow.keyWindow 37 | #endif 38 | show( 39 | becomeKey: config.shouldBecomeKeyWindow, 40 | frame: config.windowScene?.coordinateSpace.bounds 41 | ) 42 | } 43 | 44 | private func show(becomeKey: Bool, frame: CGRect? = nil) { 45 | guard let window = window else { return } 46 | window.frame = frame ?? UIScreen.main.bounds 47 | if becomeKey { 48 | window.makeKeyAndVisible() 49 | } else { 50 | window.isHidden = false 51 | } 52 | } 53 | 54 | func uninstall() { 55 | if window?.isKeyWindow == true { 56 | previousKeyWindow?.makeKey() 57 | } 58 | window?.windowScene = nil 59 | window?.isHidden = true 60 | window = nil 61 | } 62 | 63 | required public init?(coder aDecoder: NSCoder) { 64 | fatalError("init(coder:) has not been implemented") 65 | } 66 | 67 | override open var preferredStatusBarStyle: UIStatusBarStyle { 68 | return config.preferredStatusBarStyle ?? super.preferredStatusBarStyle 69 | } 70 | 71 | open override var prefersStatusBarHidden: Bool { 72 | return config.prefersStatusBarHidden ?? super.prefersStatusBarHidden 73 | } 74 | 75 | // MARK: - Variables 76 | 77 | private var window: UIWindow? 78 | private weak var previousKeyWindow: UIWindow? 79 | 80 | let config: SwiftMessages.Config 81 | } 82 | 83 | extension WindowViewController { 84 | static func newInstance(config: SwiftMessages.Config) -> WindowViewController { 85 | return config.windowViewController?(config) ?? WindowViewController(config: config) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SwiftMessagesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftMessagesTests/SwiftMessagesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMessagesTests.swift 3 | // SwiftMessagesTests 4 | // 5 | // Created by Timothy Moose on 8/9/16. 6 | // Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftMessages 11 | 12 | class SwiftMessagesTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/Assets.xcassets/Demo message background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEB", 9 | "green" : "0xE1", 10 | "red" : "0xAC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/DemoMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoMessage.swift 3 | // SwiftUIDemo 4 | // 5 | // Created by Timothy Moose on 10/5/23. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftMessages 10 | 11 | struct DemoMessage: Identifiable { 12 | let title: String 13 | let body: String 14 | let style: DemoMessageView.Style 15 | 16 | var id: String { title + body } 17 | } 18 | 19 | extension DemoMessage: MessageViewConvertible { 20 | func asMessageView() -> DemoMessageView { 21 | DemoMessageView(message: self, style: style) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoMessageView.swift 3 | // SwiftUIDemo 4 | // 5 | // Created by Timothy Moose on 10/5/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // A message view with a title and message. 11 | struct DemoMessageView: View { 12 | 13 | // MARK: - API 14 | 15 | enum Style { 16 | case standard 17 | case card 18 | case tab 19 | } 20 | 21 | let message: DemoMessage 22 | let style: Style 23 | 24 | 25 | // MARK: - Variables 26 | 27 | // MARK: - Constants 28 | 29 | // MARK: - Body 30 | 31 | var body: some View { 32 | switch style { 33 | case .standard: 34 | content() 35 | // Mask the content and extend background into the safe area. 36 | .mask { 37 | Rectangle() 38 | .edgesIgnoringSafeArea(.top) 39 | } 40 | case .card: 41 | content() 42 | // Mask the content with a rounded rectangle 43 | .mask { 44 | RoundedRectangle(cornerRadius: 15) 45 | } 46 | // External padding around the card 47 | .padding(10) 48 | case .tab: 49 | content() 50 | // Mask the content with rounded bottom edge and extend background into the safe area. 51 | .mask { 52 | UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15) 53 | .edgesIgnoringSafeArea(.top) 54 | } 55 | } 56 | } 57 | 58 | @ViewBuilder private func content() -> some View { 59 | VStack(alignment: .leading) { 60 | Text(message.title).font(.system(size: 20, weight: .bold)) 61 | Text(message.body) 62 | } 63 | .multilineTextAlignment(.leading) 64 | // Internal padding of the card 65 | .padding(30) 66 | // Greedy width 67 | .frame(maxWidth: .infinity) 68 | .background(.demoMessageBackground) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SwiftUIDemo/SwiftUIDemo/DemoMessageWithButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoMessageWithButtonView.swift 3 | // SwiftUIDemo 4 | // 5 | // Created by Timothy Moose on 1/15/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // A message view with a title, message and button. 11 | struct DemoMessageWithButtonView 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /iMessageDemo/iMessageExtensionDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | iMessageExtensionDemo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | NSExtension 24 | 25 | NSExtensionMainStoryboard 26 | MainInterface 27 | NSExtensionPointIdentifier 28 | com.apple.message-payload-provider 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /iMessageDemo/iMessageExtensionDemo/MessagesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagesViewController.swift 3 | // iMessageDemo 4 | // 5 | // Created by Timothy Moose on 5/25/18. 6 | // Copyright © 2018 SwiftKick Mobile. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Messages 11 | import SwiftMessages 12 | 13 | class MessagesViewController: MSMessagesAppViewController { 14 | 15 | @IBOutlet weak var button: UIButton! { 16 | didSet { 17 | button.layer.cornerRadius = 5 18 | } 19 | } 20 | 21 | @IBAction func buttonTapped() { 22 | let messageView: MessageView = MessageView.viewFromNib(layout: .centeredView) 23 | messageView.configureContent(title: "Test", body: "Yep, it works!") 24 | messageView.button?.isHidden = true 25 | messageView.iconLabel?.isHidden = true 26 | messageView.iconImageView?.isHidden = true 27 | messageView.configureTheme(backgroundColor: .black, foregroundColor: .white) 28 | messageView.configureDropShadow() 29 | messageView.configureBackgroundView(width: 200) 30 | var config = SwiftMessages.defaultConfig 31 | config.presentationStyle = .center 32 | config.presentationContext = .viewController(self) 33 | SwiftMessages.show(config: config, view: messageView) 34 | } 35 | } 36 | --------------------------------------------------------------------------------