├── .gitattributes
├── .github
└── workflows
│ └── Build.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Assets
├── activity_demo.gif
├── custom_demo.gif
├── success_demo.gif
└── warning_demo.gif
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── MessageViewUI
│ ├── Extensions.swift
│ ├── MessageView.swift
│ ├── MessageViewBuilder.swift
│ ├── MessageViewBuilderStyles.swift
│ └── MessageViewStyle.swift
├── Tests
├── LinuxMain.swift
└── MessageViewUITests
│ ├── MessageViewUITests.swift
│ └── XCTestManifests.swift
└── logo-message_view.jpg
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.podspec linguist-detectable=false
2 | *.ruby linguist-detectable=false
3 | *.ru linguist-detectable=false
4 | *.spec linguist-detectable=false
5 | *.rbx linguist-detectable=false
6 | *.rabl linguist-detectable=false
7 |
--------------------------------------------------------------------------------
/.github/workflows/Build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: macOS-latest
6 | steps:
7 | - uses: actions/checkout@v1
8 |
9 | - name: Switch to Xcode 11
10 | run: sudo xcode-select --switch /Applications/Xcode_11.3.app
11 | # Since we want to be running our tests from Xcode, we need to
12 | # generate an .xcodeproj file. Luckly, Swift Package Manager has
13 | # build in functionality to do so.
14 | - name: Generate xcodeproj
15 | run: swift package generate-xcodeproj
16 | - name: Run tests
17 | run: xcodebuild test -destination 'name=iPhone 11' -scheme 'MessageViewUI-Package'
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Assets/activity_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eleev/message-view-ui/a2dc5222c08fe46ffd2ee1a30e8ff62edfa609a8/Assets/activity_demo.gif
--------------------------------------------------------------------------------
/Assets/custom_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eleev/message-view-ui/a2dc5222c08fe46ffd2ee1a30e8ff62edfa609a8/Assets/custom_demo.gif
--------------------------------------------------------------------------------
/Assets/success_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eleev/message-view-ui/a2dc5222c08fe46ffd2ee1a30e8ff62edfa609a8/Assets/success_demo.gif
--------------------------------------------------------------------------------
/Assets/warning_demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eleev/message-view-ui/a2dc5222c08fe46ffd2ee1a30e8ff62edfa609a8/Assets/warning_demo.gif
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Astemir Eleev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MessageViewUI",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "MessageViewUI",
15 | targets: ["MessageViewUI"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "MessageViewUI",
26 | dependencies: []),
27 | .testTarget(
28 | name: "MessageViewUITests",
29 | dependencies: ["MessageViewUI"]),
30 | ],
31 | swiftLanguageVersions: [
32 | .v5
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # message-view-ui [](https://github.com/sindresorhus/awesome)
2 |
3 | []()
4 | []()
5 | []()
6 | []()
7 | []()
8 | []()
9 |
10 | **Last Update: 03/January/2020.**
11 |
12 | 
13 |
14 | ### If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo.
15 |
16 | # ✍️ About
17 | ✉️ Easy to use HUD component for iOS [activity report, success or warning messages, etc.]
18 |
19 | # 📺 Demo
20 | Please wait while the `.gif` files are loading... (they are about `25Mb`)
21 |
22 |
23 |
24 | # 🍱 Features
25 |
26 | - **Easy to use**
27 | - You only need to make a call to a function though `MessageView` class.
28 | - **Flexible `API`**
29 | - Includes a number of customization points that allows to decorate the `MessageView` as you'd like.
30 | - **Styling**
31 | - You may implement various visual styles by conforming to `MessageViewBuilder` protocol and supplying an instance of your style to `configure` method of `MessageView` class.
32 | - **Behavior**
33 | - You may tell the component to dismiss itself after a number of seconds or do it manually.
34 | - **Autolayout**
35 | - You don't need to do anything related to autolayout - the component properly handles everything.
36 | - **customize icons**
37 | - You can supply your own icon and programmatically set its color.
38 |
39 | # 📚 Code Samples
40 |
41 | ### Activity
42 | Presents message with an activity indicator view. The intention behind this presentation is to report some long running task:
43 |
44 | ```swift
45 | MessageView.showActivity(withMessage: "Please wait...", dismissAfter: 3.0)
46 | ```
47 |
48 | Or you can omit the `dismissAfter` parameter and hide the `MessageView` manually by calling `hide` method:
49 |
50 | ```swift
51 | MessageView.hide()
52 | ```
53 |
54 | ### Success
55 | Presents a success message. The intention behind this presentation is to report that something was successful or completed:
56 |
57 | ```swift
58 | MessageView.showSuccess(withMessage: "Success!", dismissAfter: 2.25)
59 | ```
60 |
61 | ### Warning
62 | Presents a warning message. The intention behind this presentation is to report that something wasn't successful or failed:
63 |
64 | ```swift
65 | MessageView.showWarning(withMessage: "Warning!", dismissAfter: 2.5)
66 | ```
67 |
68 | ### Custom
69 | Presents a custom image above the message. The intention behind this presentation style is defined by you, the developer. For instance we can present a failure `MessageView` by providing `failureImage` and the corresponding `tintColor`:
70 |
71 | ```swift
72 | MessageView.showCustom(image: failureImage,
73 | tintColor: .red,
74 | withMessage: "Something went wrong",
75 | dismissAfter: 2.8)
76 | ```
77 |
78 | ### Message update
79 | Message updates are used in order to refresh the text message inside a `MessageView` while it's presented on screen. It's useful is situations when a long running task is in progress and we need to report various stages of the progress:
80 |
81 | ```swift
82 | MessageView.showActivity(withMessage: “Initializing the task...")
83 |
84 | fetcher.fetch(data) { result in
85 | MessageView.update(message: "Completed! Result is \(result)", dismissAfter: 3.0)
86 | handle(result)
87 | }.progress { value in
88 | MessageView.update(message: "Fetching: \(value)%")
89 | }
90 | ```
91 |
92 | ### Styles
93 | There is a protocol called `MessageViewBuilder` that defines a number of properties. By creating your own version of style or by using one of the existing styles, you can customize the visuals of the component:
94 |
95 | ```swift
96 | MessageView.configure(with: .dark)
97 | MessageView.configure(with: .extraLight)
98 | MessageView.configure(with: .default)
99 | ```
100 |
101 | Or you can use an alternative `configure` method with your own version of style type:
102 |
103 | ```swift
104 | public struct MessageViewNightStyleBuilder: MessageViewBuilder {
105 | public var activityIndicatorColor: UIColor = .init(red: 252 / 256, green: 0.0, blue: 0.0, alpha: 1.0)
106 | public var messageColor: UIColor = .init(red: 71 / 256, green: 68 / 256, blue: 69 / 256, alpha: 1.0)
107 | public var messageFont: UIFont = UIFont.systemFont(ofSize: 20)
108 | public var animationDuration: TimeInterval = 0.475
109 | public var loadingIndicatorSize: CGFloat = 55
110 | public var loadingIndicatorInitialTransform: CGAffineTransform = CGAffineTransform(scaleX: 0.12, y: 0.12)
111 | public var successColor: UIColor = .init(red: 0.0, green: 134 / 256, blue: 245 / 256, alpha: 1.0)
112 | public var warningColor: UIColor = .init(red: 245 / 256, green: 0.0, blue: 0.0, alpha: 1.0)
113 | public var backgroundStyle: MessageView.BackgroundStyle = .dark
114 | }
115 |
116 | MessageView.configure(with: MessageViewNightStyleBuilder())
117 | ```
118 |
119 | ## Swift Package Manager
120 |
121 | ### Xcode 11+
122 |
123 | 1. Open `MenuBar` → `File` → `Swift Packages` → `Add Package Dependency...`
124 | 2. Paste the package repository url `https://github.com/jVirus/message-view-ui` and hit `Next`.
125 | 3. Select the installment rules.
126 |
127 | After specifying which version do you want to install, the package will be downloaded and attached to your project.
128 |
129 | ### Package.swift
130 | If you already have a `Package.swift` or you are building your own package simply add a new dependency:
131 |
132 | ```swift
133 | dependencies: [
134 | .package(url: "`https://github.com/jVirus/message-view-ui", from: "1.0.0")
135 | ]
136 | ```
137 |
138 | ## Manual
139 | You can always use copy-paste the sources method 😄. Or you can compile the framework and include it with your project.
140 |
141 |
142 | # 👨💻 Author
143 | [Astemir Eleev](https://github.com/jVirus)
144 |
145 | # 🔖 Licence
146 | The project is available under [MIT licence](https://github.com/jVirus/message-view/blob/master/LICENSE).
147 |
--------------------------------------------------------------------------------
/Sources/MessageViewUI/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // MessageViewUI
4 | //
5 | // Created by Astemir Eleev on 21/03/2019.
6 | // Copyright © 2019 Astemir Eleev. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | internal extension CGFloat {
12 | static let smallSpacing: CGFloat = 8
13 | static let mediumSpacing: CGFloat = 16
14 | static let largeSpacing: CGFloat = 64
15 | }
16 |
17 | internal extension UIView {
18 |
19 | @discardableResult
20 | func fillInSuperview(insets: UIEdgeInsets = .zero, isActive: Bool = true) -> [NSLayoutConstraint] {
21 | guard let superview = self.superview else { return [NSLayoutConstraint]() }
22 | translatesAutoresizingMaskIntoConstraints = false
23 |
24 | var constraints = [NSLayoutConstraint]()
25 | constraints += [
26 | topAnchor.constraint(equalTo: superview.topAnchor, constant: insets.top),
27 | leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: insets.left),
28 | bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: insets.bottom),
29 | trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: insets.right)
30 | ]
31 | if isActive { NSLayoutConstraint.activate(constraints) }
32 |
33 | return constraints
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/MessageViewUI/MessageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageView.swift
3 | // MessageViewUI
4 | //
5 | // Created by Astemir Eleev on 21/03/2019.
6 | // Copyright © 2019 Astemir Eleev. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// MessageView is HUD component that is intended to be used in cases when success or failure messages and activity progress need to be reported to the user. The usage of the component is quite straingforward:
12 | ///
13 | /// - shared instance - yes, singletons are bad, but in some cases they are useful. You will not be able to create broken state, unless you use concurrently will present different MessageViews. This is the preffered and quite convenient way to use this component in the majority of cases.
14 | ///
15 | /// The visials can be customized by using the build-in styles [MessageViewBuilder](MessageViewBuilder.swift), or you can create your own presentations and use the `configure` method to change the default appearence.
16 | public final class MessageView: UIView {
17 |
18 | // MARK: - Private properties
19 |
20 | private var builder: MessageViewBuilder = MessageViewDefaultBuilder() {
21 | didSet {
22 | loadingIndicator.transform = builder.loadingIndicatorInitialTransform
23 | loadingIndicator.color = builder.activityIndicatorColor
24 |
25 | messageLabel.font = builder.messageFont
26 | messageLabel.textColor = builder.messageColor
27 |
28 | imageView.transform = builder.loadingIndicatorInitialTransform
29 |
30 | blurEffectView?.removeFromSuperview()
31 | blurEffectView = createVisualEffectView()
32 | prepareBackground()
33 | }
34 | }
35 |
36 | private var state: State = .hidden
37 | private static let shared = MessageView()
38 |
39 | private lazy var loadingIndicator: UIActivityIndicatorView = {
40 | let view = UIActivityIndicatorView(style: builder.activityIndicatorStyle)
41 | view.translatesAutoresizingMaskIntoConstraints = false
42 | view.transform = builder.loadingIndicatorInitialTransform
43 | view.color = builder.activityIndicatorColor
44 | return view
45 | }()
46 |
47 | private lazy var messageLabel: UILabel = {
48 | let label = UILabel(frame: .zero)
49 | label.font = builder.messageFont
50 | label.translatesAutoresizingMaskIntoConstraints = false
51 | label.textAlignment = .center
52 | label.numberOfLines = 0
53 | label.textColor = builder.messageColor
54 | return label
55 | }()
56 |
57 | private lazy var imageView: UIImageView = {
58 | let view = UIImageView()
59 | view.translatesAutoresizingMaskIntoConstraints = false
60 | view.tintColor = builder.successColor
61 | view.alpha = 0
62 | view.transform = builder.loadingIndicatorInitialTransform
63 | return view
64 | }()
65 |
66 | fileprivate lazy var blurEffectView: UIVisualEffectView? = {
67 | createVisualEffectView()
68 | }()
69 |
70 | private var defaultWindow: UIWindow?
71 |
72 | // MARK: - Initializers
73 |
74 | private init(window: UIWindow? = UIApplication.shared.windows.first) {
75 | super.init(frame: .zero)
76 | defaultWindow = window
77 | alpha = 0
78 | translatesAutoresizingMaskIntoConstraints = false
79 | setup()
80 | }
81 |
82 | public required init?(coder aDecoder: NSCoder) {
83 | fatalError("Try to use the shared instance instead")
84 | }
85 |
86 | // MARK: - Methods
87 |
88 | /// Presents message with an activity indicator view. The intention behind this presentation is to report some long running task.
89 | ///
90 | /// - Parameters:
91 | /// - message: is an optioanl `String` message that will be displayed below the activity indicator
92 | /// - delay: is a `TimeInterval` parameter that delays the presentation of the component
93 | /// - interval: is a `TimeInterval` parameter that will be used to dismiss the component after the specified number of seconds (default is `0.0`, which means that the component needs to be dismissed **manually**)
94 | public class func showActivity(withMessage message: String? = nil,
95 | afterDelay delay: TimeInterval = 0.25,
96 | dismissAfter interval: TimeInterval = 0.0) {
97 | State.message.getExecutable(with: message, delay: delay)()
98 | hideIfDelayed(interval)
99 | }
100 |
101 | /// Presents a success message. The intention behind this presentation is to report that something was successfull or completed.
102 | ///
103 | /// - Parameters:
104 | /// - message: is an optioanl `String` message that will be displayed below the activity indicator
105 | /// - delay: is a `TimeInterval` parameter that delays the presentation of the component
106 | /// - interval: is a `TimeInterval` parameter that will be used to dismiss the component after the specified number of seconds (default is `0.0`, which means that the component needs to be dismissed **manually**)
107 | public class func showSuccess(withMessage message: String? = nil,
108 | afterDelay delay: TimeInterval = 0.25,
109 | dismissAfter interval: TimeInterval = 0.0) {
110 | State.success.getExecutable(with: message, delay: delay)()
111 | hideIfDelayed(interval)
112 | }
113 |
114 | /// Presents a warning message. The intention behind this presentation is to report that something wasn't successfull or failed.
115 | ///
116 | /// - Parameters:
117 | /// - message: is an optioanl `String` message that will be displayed below the activity indicator
118 | /// - delay: is a `TimeInterval` parameter that delays the presentation of the component
119 | /// - interval: is a `TimeInterval` parameter that will be used to dismiss the component after the specified number of seconds (default is `0.0`, which means that the component needs to be dismissed **manually**)
120 | public class func showWarning(withMessage message: String? = nil,
121 | afterDelay delay: TimeInterval = 0.25,
122 | dismissAfter interval: TimeInterval = 0.0) {
123 | State.warning.getExecutable(with: message, delay: delay)()
124 | hideIfDelayed(interval)
125 | }
126 |
127 | /// Presents a custom image above the message. The intention behing this presentation style is defined by you, the developer.
128 | ///
129 | /// - Parameters:
130 | /// - image: is a `UIImage` parameter that holds a valid image that will be presented above the text message. The image should be white on transparent background, since the rendering mode is `alwaysTemplate`.
131 | /// - color: is a `UIColor` parameter that is used to color the image
132 | /// - message: is an optioanl `String` message that will be displayed below the activity indicator
133 | /// - delay: is a `TimeInterval` parameter that delays the presentation of the component
134 | /// - interval: is a `TimeInterval` parameter that will be used to dismiss the component after the specified number of seconds (default is `0.0`, which means that the component needs to be dismissed **manually**)
135 | public class func showCustom(image: UIImage,
136 | tintColor color: UIColor = .lightGray,
137 | withMessage message: String? = nil,
138 | afterDelay delay: TimeInterval = 0.25,
139 | dismissAfter interval: TimeInterval = 0.0) {
140 | State.custom(image, color).getExecutable(with: message, delay: delay)()
141 | hideIfDelayed(interval)
142 | }
143 |
144 | /// Hides the MessageView immediately
145 | public class func hide() {
146 | State.hidden.getExecutable(with: nil, delay: 0.0)()
147 | }
148 |
149 | /// Hides the MessageView after the specified delay
150 | public class func hide(afterDelay delay: TimeInterval) {
151 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
152 | hide()
153 | })
154 | }
155 |
156 | /// Configuration point. Create your own type that conforms to `MessageViewBuilder` protocol and supply it in order to customize the visuals.
157 | public class func configure(with builder: MessageViewBuilder) {
158 | MessageView.shared.builder = builder
159 | }
160 |
161 | /// Configuration point. Create your own type that conforms to `MessageViewBuilder` protocol, wrap it into `MessageViewStyle` enum type and use more convenient configuration style.
162 | public class func configure(with style: MessageViewStyle) {
163 | MessageView.shared.builder = style.getBuilder()
164 | }
165 |
166 | /// Updates the text message for the presented `MessageView`. Useful in cases when a single instnace of a `MessageView` needs to be updated without hiding and presenting a new component.
167 | ///
168 | /// - Parameters:
169 | /// - message: a new `String` message that will be displayed
170 | /// - interval: is a `TimeInterval` parameter that will be used to dismiss the component after the specified number of seconds (default is `0.0`, which means that the component needs to be dismissed **manually**)
171 | public class func update(message: String, dismissAfter interval: TimeInterval = 0.0) {
172 | DispatchQueue.main.async {
173 | MessageView.shared.messageLabel.text = message
174 | hideIfDelayed(interval)
175 | }
176 | }
177 | }
178 |
179 | // MARK: - Private extension
180 |
181 | private extension MessageView {
182 |
183 | // MARK: - Methods
184 |
185 | private class func hideIfDelayed(_ interval: TimeInterval) {
186 | guard interval > 0.0 else { return }
187 | hide(afterDelay: interval)
188 | }
189 |
190 | private func createVisualEffectView() -> UIVisualEffectView? {
191 | guard let style = builder.backgroundStyle.getBlurStyle() else { return nil }
192 | let blurEffect = UIBlurEffect(style: style)
193 | let effectView = UIVisualEffectView(effect: blurEffect)
194 | return effectView
195 | }
196 |
197 | private func prepareBackground() {
198 | switch builder.backgroundStyle {
199 | case .color(let instance):
200 | backgroundColor = instance
201 | case .dark, .light, .extraLight:
202 | guard let blurEffectView = blurEffectView else { return }
203 | insertSubview(blurEffectView, belowSubview: self)
204 | backgroundColor = .clear
205 | insertSubview(blurEffectView, at: 0)
206 | blurEffectView.fillInSuperview()
207 | }
208 | }
209 |
210 | private func setup() {
211 | func prepareUIComposition() {
212 | addSubview(loadingIndicator)
213 | addSubview(messageLabel)
214 | addSubview(imageView)
215 |
216 | NSLayoutConstraint.activate([
217 | imageView.widthAnchor.constraint(equalToConstant: builder.loadingIndicatorSize),
218 | imageView.heightAnchor.constraint(equalToConstant: builder.loadingIndicatorSize - .smallSpacing),
219 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
220 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -.smallSpacing),
221 |
222 | loadingIndicator.widthAnchor.constraint(equalToConstant: builder.loadingIndicatorSize),
223 | loadingIndicator.heightAnchor.constraint(equalToConstant: builder.loadingIndicatorSize),
224 | loadingIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
225 | loadingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -.smallSpacing),
226 |
227 | messageLabel.topAnchor.constraint(equalTo: loadingIndicator.bottomAnchor, constant: .mediumSpacing),
228 | messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: .largeSpacing),
229 | messageLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -.largeSpacing)
230 | ])
231 | }
232 |
233 | prepareBackground()
234 | prepareUIComposition()
235 | }
236 |
237 | private func showImage(withMessage message: String? = nil) {
238 | if superview == nil {
239 | defaultWindow?.addSubview(self)
240 | fillInSuperview()
241 | }
242 | let data = state.getData(from: builder)
243 | imageView.image = data.image
244 | imageView.tintColor = data.color
245 |
246 | loadingIndicator.alpha = 0
247 | messageLabel.text = message
248 | blurEffectView?.alpha = 0
249 |
250 | UIView.animate(withDuration: builder.animationDuration) {
251 | self.alpha = 1
252 | self.blurEffectView?.alpha = 1
253 | self.imageView.alpha = 1
254 | self.imageView.transform = .identity
255 | }
256 | }
257 |
258 | private func startAnimating(withMessage message: String? = nil) {
259 | if superview == nil {
260 | defaultWindow?.addSubview(self)
261 | fillInSuperview()
262 | }
263 | messageLabel.text = message
264 | imageView.alpha = 0
265 | blurEffectView?.alpha = 0
266 | loadingIndicator.startAnimating()
267 | loadingIndicator.transform = builder.loadingIndicatorInitialTransform
268 | loadingIndicator.alpha = 1
269 |
270 | UIViewPropertyAnimator(duration: builder.animationDuration, curve: .easeIn) {
271 | self.alpha = 1
272 | self.blurEffectView?.alpha = 1
273 | self.loadingIndicator.transform = .identity
274 | }.startAnimation()
275 | }
276 |
277 | private func stopAnimating() {
278 | if defaultWindow != nil {
279 | let transform = builder.loadingIndicatorInitialTransform
280 |
281 | UIView.animate(withDuration: builder.animationDuration, animations: {
282 | self.loadingIndicator.transform = transform
283 | self.imageView.transform = transform
284 | }, completion: { _ in
285 | self.loadingIndicator.stopAnimating()
286 | self.messageLabel.text = nil
287 |
288 | UIView.animate(withDuration: self.builder.animationDuration, animations: {
289 | self.alpha = 0
290 | self.blurEffectView?.alpha = 0
291 | }, completion: { _ in
292 | self.removeFromSuperview()
293 | })
294 | })
295 | }
296 | }
297 | }
298 |
299 | // MARK: - Enum type extension
300 | public extension MessageView {
301 |
302 | // MARK: - Enum types
303 |
304 | /// Private state of the component. Incapsulates the possible cases such as `message` or `success`. Also, it provides conveninet factory methods that produce either executables or data depending on the current state.
305 | private enum State: Equatable {
306 |
307 | // MARK: - Typealisases
308 |
309 | typealias Data = (image: UIImage?, color: UIColor?)
310 |
311 | // MARK: - Cases
312 |
313 | case message
314 | case hidden
315 | case success
316 | case warning
317 | case custom(UIImage, UIColor)
318 |
319 | // MARK: - Methods
320 |
321 | func getData(from model: MessageViewBuilder) -> Data {
322 | switch self {
323 | case .success:
324 | return (UIImage(systemName: "checkmark.circle",
325 | withConfiguration: UIImage.SymbolConfiguration.init(scale: .large))?
326 | .withRenderingMode(.alwaysTemplate),
327 | model.successColor)
328 | case .warning:
329 | return (UIImage(systemName: "exclamationmark.triangle",
330 | withConfiguration: UIImage.SymbolConfiguration.init(scale: .large))?
331 | .withRenderingMode(.alwaysTemplate),
332 | model.warningColor)
333 | case .custom(let image, let color):
334 | return (image, color)
335 | default:
336 | return (nil, nil)
337 | }
338 | }
339 |
340 | func getExecutable(with message: String?, delay: TimeInterval) -> () -> Void {
341 | switch self {
342 | case .success:
343 | return {
344 | MessageView.shared.state = .success
345 |
346 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
347 | if MessageView.shared.state == .success {
348 | MessageView.shared.showImage(withMessage: message)
349 | }
350 | })
351 | }
352 | case .message:
353 | return {
354 | MessageView.shared.state = .message
355 |
356 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
357 | if MessageView.shared.state == .message {
358 | MessageView.shared.startAnimating(withMessage: message)
359 | }
360 | })
361 | }
362 | case .warning:
363 | return {
364 | MessageView.shared.state = .warning
365 |
366 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
367 | if MessageView.shared.state == .warning {
368 | MessageView.shared.showImage(withMessage: message)
369 | }
370 | })
371 | }
372 | case .custom(let image, let tint):
373 | return {
374 | MessageView.shared.state = .custom(image, tint)
375 |
376 | DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: {
377 | if case .custom = MessageView.shared.state {
378 | MessageView.shared.showImage(withMessage: message)
379 | }
380 | })
381 | }
382 | case .hidden:
383 | return {
384 | MessageView.shared.state = .hidden
385 | MessageView.shared.stopAnimating()
386 | }
387 | }
388 | }
389 | }
390 |
391 | enum BackgroundStyle: Equatable {
392 |
393 | // MARK: - Cases
394 |
395 | case color(UIColor)
396 | case light
397 | case extraLight
398 | case dark
399 |
400 | // MARK: - Methods
401 |
402 | func getBlurStyle() -> UIBlurEffect.Style? {
403 | switch self {
404 | case .light:
405 | return .light
406 | case .extraLight:
407 | return .extraLight
408 | case .dark:
409 | return .dark
410 | default:
411 | return nil
412 | }
413 | }
414 | }
415 | }
416 |
--------------------------------------------------------------------------------
/Sources/MessageViewUI/MessageViewBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageViewBuilder.swift
3 | // MessageViewUI
4 | //
5 | // Created by Astemir Eleev on 21/03/2019.
6 | // Copyright © 2019 Astemir Eleev. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Builder protocol that decomposes the data needed to configure the `MessageView` class.
12 | public protocol MessageViewBuilder {
13 | var activityIndicatorColor: UIColor { get }
14 | var activityIndicatorStyle: UIActivityIndicatorView.Style { get }
15 | var messageColor: UIColor { get }
16 | var messageFont: UIFont { get }
17 | var animationDuration: TimeInterval { get }
18 | var loadingIndicatorSize: CGFloat { get }
19 | var loadingIndicatorInitialTransform: CGAffineTransform { get }
20 | var successColor: UIColor { get }
21 | var warningColor: UIColor { get }
22 | var backgroundStyle: MessageView.BackgroundStyle { get }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/MessageViewUI/MessageViewBuilderStyles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageViewBuilderStyles.swift
3 | // MessageViewUI
4 | //
5 | // Created by Astemir Eleev on 21/03/2019.
6 | // Copyright © 2019 Astemir Eleev. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public struct MessageViewDefaultBuilder: MessageViewBuilder {
12 | public var activityIndicatorColor: UIColor = .init(red: 0.0, green: 99 / 256, blue: 251 / 256, alpha: 1.0)
13 | public var activityIndicatorStyle: UIActivityIndicatorView.Style = .large
14 | public var messageColor: UIColor = .init(red: 71 / 256, green: 68 / 256, blue: 69 / 256, alpha: 1.0)
15 | public var messageFont: UIFont = UIFont.systemFont(ofSize: 16)
16 | public var animationDuration: TimeInterval = 0.35
17 | public var loadingIndicatorSize: CGFloat = 45
18 | public var loadingIndicatorInitialTransform: CGAffineTransform = CGAffineTransform(scaleX: 0.01, y: 0.01)
19 | public var successColor: UIColor = .init(red: 0.0, green: 134 / 256, blue: 245 / 256, alpha: 1.0)
20 | public var warningColor: UIColor = .init(red: 245 / 256, green: 0.0, blue: 0.0, alpha: 1.0)
21 | public var backgroundStyle: MessageView.BackgroundStyle = .color(UIColor.white.withAlphaComponent(0.85))
22 | }
23 |
24 | public struct MessageViewDarkBlurBuilder: MessageViewBuilder {
25 | public var activityIndicatorColor: UIColor = .init(red: 35 / 256, green: 158 / 256, blue: 242 / 256, alpha: 1.0)
26 | public var activityIndicatorStyle: UIActivityIndicatorView.Style = .large
27 | public var messageColor: UIColor = .init(red: 224 / 256, green: 200 / 256, blue: 220 / 256, alpha: 1.0)
28 | public var messageFont: UIFont = UIFont.systemFont(ofSize: 16)
29 | public var animationDuration: TimeInterval = 0.35
30 | public var loadingIndicatorSize: CGFloat = 45
31 | public var loadingIndicatorInitialTransform: CGAffineTransform = CGAffineTransform(scaleX: 0.01, y: 0.01)
32 | public var successColor: UIColor = .init(red: 0.0, green: 134 / 256, blue: 245 / 256, alpha: 1.0)
33 | public var warningColor: UIColor = .init(red: 245 / 256, green: 0.0, blue: 0.0, alpha: 1.0)
34 | public var backgroundStyle: MessageView.BackgroundStyle = .dark
35 | }
36 |
37 | public struct MessageViewExtraLightBlurBuilder: MessageViewBuilder {
38 | public var activityIndicatorColor: UIColor = .init(red: 128 / 256, green: 128 / 256, blue: 128 / 256, alpha: 1.0)
39 | public var activityIndicatorStyle: UIActivityIndicatorView.Style = .large
40 | public var messageColor: UIColor = .init(red: 64 / 256, green: 64 / 256, blue: 64 / 256, alpha: 1.0)
41 | public var messageFont: UIFont = UIFont.systemFont(ofSize: 16)
42 | public var animationDuration: TimeInterval = 0.35
43 | public var loadingIndicatorSize: CGFloat = 45
44 | public var loadingIndicatorInitialTransform: CGAffineTransform = CGAffineTransform(scaleX: 0.01, y: 0.01)
45 | public var successColor: UIColor = .init(red: 0.0, green: 134 / 256, blue: 245 / 256, alpha: 1.0)
46 | public var warningColor: UIColor = .init(red: 245 / 256, green: 0.0, blue: 0.0, alpha: 1.0)
47 | public var backgroundStyle: MessageView.BackgroundStyle = .extraLight
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/MessageViewUI/MessageViewStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageViewStyle.swift
3 | // MessageViewUI
4 | //
5 | // Created by Astemir Eleev on 21/03/2019.
6 | // Copyright © 2019 Astemir Eleev. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A convenience enum wrapper around the built-in `MessageViewBuilder` conforming types
12 | public enum MessageViewStyle {
13 | case `default`
14 | case dark
15 | case extraLight
16 | }
17 |
18 | public extension MessageViewStyle {
19 | func getBuilder() -> MessageViewBuilder {
20 | switch self {
21 | case .default:
22 | return MessageViewDefaultBuilder()
23 | case .dark:
24 | return MessageViewDarkBlurBuilder()
25 | case .extraLight:
26 | return MessageViewExtraLightBlurBuilder()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import MessageViewUITests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += MessageViewUITests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/MessageViewUITests/MessageViewUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MessageViewUI
3 |
4 | final class MessageViewUITests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 |
11 | static var allTests = [
12 | ("testExample", testExample),
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/MessageViewUITests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(MessageViewUITests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/logo-message_view.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eleev/message-view-ui/a2dc5222c08fe46ffd2ee1a30e8ff62edfa609a8/logo-message_view.jpg
--------------------------------------------------------------------------------