├── .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