├── .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 [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 2 | 3 | [![Build](https://github.com/jvirus/message-view-ui/workflows/Build/badge.svg)]() 4 | [![Platforms](https://img.shields.io/badge/Platform-iOS-yellow.svg)]() 5 | [![Language](https://img.shields.io/badge/Language-Swift_5.1-orange.svg)]() 6 | [![Autolayout](https://img.shields.io/badge/Autolayout-Supported-blue.svg)]() 7 | [![SPM](https://img.shields.io/badge/SPM-Supported-lightblue.svg)]() 8 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() 9 | 10 | **Last Update: 03/January/2020.** 11 | 12 | ![](logo-message_view.jpg) 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 --------------------------------------------------------------------------------