├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Popup │ ├── Animation+ext.swift │ ├── PopUp.swift │ ├── PopUpViewModifier.swift │ ├── Settings │ ├── BackgroundMaterial.swift │ ├── DismissAnimation.swift │ ├── DismissOnBackgroundTap.swift │ └── PresentAnimation.swift │ └── View+ext.swift └── img └── example.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Max Gribov 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.9 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: "PopUp", 8 | platforms: [.iOS(.v15), .macOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "PopUp", 13 | targets: ["PopUp"]), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package, defining a module or a test suite. 17 | // Targets can depend on other targets in this package and products from dependencies. 18 | .target( 19 | name: "PopUp"), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PopUp 2 | SwiftUI simple pop up view implementation: 3 | 4 | drawing 5 | 6 | ## Installing 7 | PopUp Library can be installed using Swift Package Manager. 8 | 9 | Use the package URL to search for the PopUp package: [https://github.com/maxgribov/PopUp](https://github.com/maxgribov/PopUp) 10 | 11 | For how-to integrate package dependencies refer to [Adding Package Dependencies to Your App documentation.](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) 12 | 13 | ## Usage 14 | 15 | ### Show 16 | 17 | You can easily present a pop up with `View` modifier [popUp](Sources/Popup/View%2Bext.swift#L76). This is very similar how to present a sheet: 18 | 19 | ```swift 20 | struct CustomAlertModel: Identifiable { 21 | var id: UUID = UUID() 22 | let title: String 23 | let message: String 24 | } 25 | 26 | struct ContentView: View { 27 | @State var customAlert: CustomAlertModel? 28 | var body: some View { 29 | 30 | Button("Show custom alert") { 31 | customAlert = CustomAlertModel( 32 | title: "Pop Up", 33 | message: "Some message here..." 34 | ) 35 | } 36 | .popUp(item: $customAlert) { viewModel in 37 | CustomAlert(viewModel: viewModel) 38 | } 39 | } 40 | } 41 | 42 | struct CustomAlert: View { 43 | let viewModel: CustomAlertModel 44 | var body: some View { 45 | VStack { 46 | Text(viewModel.title) 47 | .font(.title) 48 | Text(viewModel.message) 49 | } 50 | .padding(40) 51 | .background( 52 | RoundedRectangle(cornerRadius: 30) 53 | .foregroundStyle(.white) 54 | ) 55 | } 56 | } 57 | ``` 58 | ### Customise 59 | 60 | There are few methods that you can use to customise look and behaviour of the pop up: 61 | - [popUpBackgroundMaterial](Sources/Popup/Settings/BackgroundMaterial.swift#L35): updates material for the dim view under the pop up view. 62 | - [popUpPresentAnimation](Sources/Popup/Settings/PresentAnimation.swift#L34): replaces the animation for the pop up show up. 63 | - [popUpDismissAnimation](Sources/Popup/Settings/DismissAnimation.swift#L34): replaces the animation for the pop up dismiss. 64 | - [popUpDismissOnBackgroundTap](Sources/Popup/Settings/DismissOnBackgroundTap.swift#L34): enables or disables the pop up dismissal on user tap at the dim view outside of the pop up view. 65 | 66 | #### Example: 67 | ```swift 68 | struct ContentView: View { 69 | @State var customAlert: CustomAlertModel? 70 | var body: some View { 71 | 72 | Group { 73 | // ... 74 | } 75 | .popUp(item: $customAlert) { viewModel in 76 | CustomAlert(viewModel: viewModel) 77 | } 78 | .popUpBackgroundMaterial(.ultraThin) 79 | } 80 | } 81 | ``` 82 | 83 | ## Notes 84 | 85 | > PopUp does not have any background or a style for the content (like the system alert view is for example). The look of your popup you have implement by yourself. 86 | 87 | > This is not a modal view (like a sheet for example). It can not cover views above it. 88 | 89 | > It is possible to open many pop ups one above other. There is no limitations like for the sheet. 90 | 91 | ## System Requirements 92 | 93 | **Swift 5.9** 94 | * iOS 15+ 95 | * macOS 12+ 96 | 97 | ## License 98 | 99 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 100 | -------------------------------------------------------------------------------- /Sources/Popup/Animation+ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Animation { 11 | 12 | static let defaultPopUpPresent: Animation = .interactiveSpring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.25) 13 | static let defaultPopUpDismiss: Animation = .easeOut(duration: 0.3) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Popup/PopUp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Popup.swift 3 | // 4 | // 5 | // Created by Max Gribov on 26.01.2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct PopUp: ViewModifier where Item : Identifiable, ItemView : View { 12 | 13 | @Binding var item: Item? 14 | @ViewBuilder let makeItemView: (Item) -> ItemView 15 | let onDismiss: (() -> Void)? 16 | 17 | func body(content: Content) -> some View { 18 | 19 | ZStack { 20 | 21 | content 22 | .zIndex(0) 23 | 24 | if let item = item { 25 | 26 | makeItemView(item) 27 | .transition(.popup(dismiss: { self.item = nil })) 28 | .zIndex(1) 29 | .onDisappear { 30 | onDismiss?() 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | struct PopUpBasic: ViewModifier where ItemView : View { 38 | 39 | @Binding var isPresented: Bool 40 | @ViewBuilder let makeItemView: () -> ItemView 41 | let onDismiss: (() -> Void)? 42 | 43 | func body(content: Content) -> some View { 44 | 45 | ZStack { 46 | 47 | content 48 | .zIndex(0) 49 | 50 | if isPresented { 51 | 52 | makeItemView() 53 | .transition(.popup(dismiss: { self.isPresented = false })) 54 | .zIndex(1) 55 | .onDisappear { 56 | onDismiss?() 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Popup/PopUpViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopUpViewModifier.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PopUpViewModifier: ViewModifier { 11 | 12 | @Environment(\.popUpBackgroundMaterial) var backgroundMaterial 13 | @Environment(\.popUpPresentAnimation) var presentAnimation 14 | @Environment(\.popUpDismissAnimation) var dismissAnimation 15 | @Environment(\.popUpDismissOnBackgroundTap) var isDismissOnBackgroundTapEnabled 16 | 17 | var transitionProgress: Double 18 | var isPresenting: Bool 19 | let dismiss: () -> Void 20 | 21 | func body(content: Content) -> some View { 22 | 23 | ZStack { 24 | 25 | Color.clear 26 | .background(backgroundMaterial) 27 | .opacity(transitionProgress) 28 | .animation(.easeInOut, value: transitionProgress) 29 | .ignoresSafeArea() 30 | .zIndex(0) 31 | .onTapGesture { 32 | if isDismissOnBackgroundTapEnabled { 33 | dismiss() 34 | } 35 | } 36 | 37 | content 38 | .scaleEffect(x: transitionProgress, y: transitionProgress) 39 | .opacity(transitionProgress) 40 | .animation(isPresenting ? presentAnimation : dismissAnimation, value: transitionProgress) 41 | .zIndex(1) 42 | } 43 | } 44 | } 45 | 46 | extension AnyTransition { 47 | 48 | static func popup(dismiss: @escaping () -> Void) -> AnyTransition { 49 | 50 | .modifier( 51 | active: PopUpViewModifier( 52 | transitionProgress: 0, 53 | isPresenting: false, 54 | dismiss: dismiss 55 | ), 56 | identity: PopUpViewModifier( 57 | transitionProgress: 1, 58 | isPresenting: true, 59 | dismiss: dismiss 60 | ) 61 | ) 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Sources/Popup/Settings/BackgroundMaterial.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundMaterial.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /** 13 | Replaces the material for the dim view that covers content 14 | under the pop up. 15 | 16 | ## Example: 17 | ```swift 18 | struct ContentView: View { 19 | @State var customAlert: CustomAlertModel? 20 | var body: some View { 21 | 22 | Group { 23 | // ... 24 | } 25 | .popUp(item: $customAlert) { viewModel in 26 | CustomAlert(viewModel: viewModel) 27 | } 28 | .popUpBackgroundMaterial(.ultraThin) 29 | } 30 | } 31 | ``` 32 | - Parameters: 33 | - value: The material to be applied to the dim view. 34 | */ 35 | func popUpBackgroundMaterial(_ value: Material) -> some View { 36 | environment(\.popUpBackgroundMaterial, value) 37 | } 38 | } 39 | 40 | //MARK: - Internal 41 | 42 | struct PopUpBackgroundMaterialKey: EnvironmentKey { 43 | static var defaultValue: Material = .regularMaterial 44 | } 45 | 46 | extension EnvironmentValues { 47 | var popUpBackgroundMaterial: Material { 48 | get { self[PopUpBackgroundMaterialKey.self] } 49 | set { self[PopUpBackgroundMaterialKey.self] = newValue } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Sources/Popup/Settings/DismissAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DismissAnimation.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /** 13 | Replaces the animation for the pop up dismiss. 14 | 15 | ## Example: 16 | ```swift 17 | struct ContentView: View { 18 | @State var customAlert: CustomAlertModel? 19 | var body: some View { 20 | 21 | Group { 22 | // ... 23 | } 24 | .popUp(item: $customAlert) { viewModel in 25 | CustomAlert(viewModel: viewModel) 26 | } 27 | .popUpDismissAnimation(.easeOut(duration: 0.2)) 28 | } 29 | } 30 | ``` 31 | - Parameters: 32 | - value: The animation to be applied to the pop up on dismiss it. 33 | */ 34 | func popUpDismissAnimation(_ value: Animation) -> some View { 35 | environment(\.popUpDismissAnimation, value) 36 | } 37 | } 38 | 39 | //MARK: - Internal 40 | 41 | struct PopUpDismissAnimationKey: EnvironmentKey { 42 | static var defaultValue: Animation = .defaultPopUpDismiss 43 | } 44 | 45 | extension EnvironmentValues { 46 | var popUpDismissAnimation: Animation { 47 | get { self[PopUpDismissAnimationKey.self] } 48 | set { self[PopUpDismissAnimationKey.self] = newValue } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Popup/Settings/DismissOnBackgroundTap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DismissOnBackgroundTap.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | /** 12 | Enables or disables the pop up dismissal on user tap at the dim view outside of the pop up view. 13 | 14 | ## Example: 15 | ```swift 16 | struct ContentView: View { 17 | @State var customAlert: CustomAlertModel? 18 | var body: some View { 19 | 20 | Group { 21 | // ... 22 | } 23 | .popUp(item: $customAlert) { viewModel in 24 | CustomAlert(viewModel: viewModel) 25 | } 26 | .popUpDismissOnBackgroundTap(false) 27 | } 28 | } 29 | ``` 30 | - Parameters: 31 | - value: A boolean value that determines whether to dismiss or not the pop up 32 | on the user tap at the dim view outside of the pop up view. Default value is `true`. 33 | */ 34 | func popUpDismissOnBackgroundTap(_ value: Bool) -> some View { 35 | environment(\.popUpDismissOnBackgroundTap, value) 36 | } 37 | } 38 | 39 | //MARK: - Internal 40 | 41 | struct PopUpDismissOnBackgroundTapKey: EnvironmentKey { 42 | static var defaultValue: Bool = true 43 | } 44 | 45 | extension EnvironmentValues { 46 | var popUpDismissOnBackgroundTap: Bool { 47 | get { self[PopUpDismissOnBackgroundTapKey.self] } 48 | set { self[PopUpDismissOnBackgroundTapKey.self] = newValue } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Popup/Settings/PresentAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentAnimation.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /** 13 | Replaces the animation for the pop up present. 14 | 15 | ## Example: 16 | ```swift 17 | struct ContentView: View { 18 | @State var customAlert: CustomAlertModel? 19 | var body: some View { 20 | 21 | Group { 22 | // ... 23 | } 24 | .popUp(item: $customAlert) { viewModel in 25 | CustomAlert(viewModel: viewModel) 26 | } 27 | .popUpPresentAnimation(.easeOut(duration: 0.2)) 28 | } 29 | } 30 | ``` 31 | - Parameters: 32 | - value: The animation to be applied to the pop up on present it. 33 | */ 34 | func popUpPresentAnimation(_ value: Animation) -> some View { 35 | environment(\.popUpPresentAnimation, value) 36 | } 37 | } 38 | 39 | //MARK: - Internal 40 | 41 | struct PopUpPresentAnimationKey: EnvironmentKey { 42 | static var defaultValue: Animation = .defaultPopUpPresent 43 | } 44 | 45 | extension EnvironmentValues { 46 | var popUpPresentAnimation: Animation { 47 | get { self[PopUpPresentAnimationKey.self] } 48 | set { self[PopUpPresentAnimationKey.self] = newValue } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Popup/View+ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ext.swift 3 | // 4 | // 5 | // Created by Max Gribov on 27.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | 12 | /** 13 | Presents a pop up using the given item as a data source 14 | for the pop up's content. 15 | 16 | Use this method when you need to present a pop up with content 17 | from a custom data source. The example below shows a custom data source 18 | `CustomAlertModel` that the `content` closure uses to populate the display 19 | the custom alert view shows to the user: 20 | 21 | ```swift 22 | struct CustomAlertModel: Identifiable { 23 | var id: UUID = UUID() 24 | let title: String 25 | let message: String 26 | } 27 | 28 | struct ContentView: View { 29 | @State var customAlert: CustomAlertModel? 30 | var body: some View { 31 | 32 | Button("Show custom alert") { 33 | customAlert = CustomAlertModel( 34 | title: "Pop Up", 35 | message: "Some message here..." 36 | ) 37 | } 38 | .popUp(item: $customAlert) { viewModel in 39 | CustomAlert(viewModel: viewModel) 40 | } 41 | } 42 | } 43 | 44 | struct CustomAlert: View { 45 | let viewModel: CustomAlertModel 46 | var body: some View { 47 | VStack { 48 | Text(viewModel.title) 49 | .font(.title) 50 | Text(viewModel.message) 51 | } 52 | .padding(40) 53 | .background( 54 | RoundedRectangle(cornerRadius: 30) 55 | .foregroundStyle(.white) 56 | ) 57 | } 58 | } 59 | ``` 60 | > This is not a modal view (like a sheet for example). 61 | It can not cover views above it. 62 | 63 | > It is possible to open many pop ups one above other. There is no limitations 64 | like for the sheet. 65 | 66 | - Parameters: 67 | - item: A binding to an optional source of truth for the pop up. 68 | When `item` is non-`nil`, the system passes the item's content to 69 | he modifier's closure. You display this content in a pop up that you 70 | create that the system displays to the user. If `item` changes, 71 | the system dismisses the pop up and replaces it with a new one 72 | using the same process. 73 | - onDismiss: The closure to execute when dismissing the pop up. 74 | - content: A closure returning the content of the pop up. 75 | */ 76 | func popUp( 77 | item: Binding, 78 | onDismiss: (() -> Void)? = nil, 79 | @ViewBuilder content: @escaping (Item) -> Content 80 | ) -> some View where Item : Identifiable, Content : View { 81 | 82 | modifier( 83 | PopUp( 84 | item: item, 85 | makeItemView: content, 86 | onDismiss: onDismiss 87 | ) 88 | ) 89 | } 90 | 91 | /** 92 | Presents a pop up when a binding to a Boolean value that you 93 | provide is true. 94 | 95 | Use this method when you want to present a pop up to the 96 | user when a Boolean value you provide is true. The example 97 | below displays a popup of the custom alert when the user 98 | toggles the `isPopup` variable by 99 | clicking or tapping on the "Show custom alert" button: 100 | 101 | ```swift 102 | struct ContentView: View { 103 | @State var isPopup: Bool = false 104 | var body: some View { 105 | 106 | Button("Show custom alert") { 107 | isPopup = true 108 | } 109 | .popUp(isPresented: $isPopup) { 110 | VStack { 111 | Text("Pop Up") 112 | .font(.title) 113 | Text("Some message here...") 114 | } 115 | .padding(40) 116 | .background( 117 | RoundedRectangle(cornerRadius: 30) 118 | .foregroundStyle(.white) 119 | ) 120 | } 121 | } 122 | } 123 | ``` 124 | > This is not a modal view (like a sheet for example). 125 | It can not cover views above it. 126 | 127 | > It is possible to open many pop ups one above other. There is no limitations 128 | like for the sheet. 129 | 130 | - Parameters: 131 | - isPresented: A binding to a Boolean value that determines whether 132 | to present the pop up that you create in the modifier's `content` closure. 133 | - onDismiss: The closure to execute when dismissing the pop up. 134 | - content: A closure that returns the content of the pop up. 135 | */ 136 | func popUp( 137 | isPresented: Binding, 138 | onDismiss: (() -> Void)? = nil, 139 | @ViewBuilder content: @escaping () -> Content 140 | ) -> some View where Content : View { 141 | 142 | modifier( 143 | PopUpBasic( 144 | isPresented: isPresented, 145 | makeItemView: content, 146 | onDismiss: onDismiss 147 | ) 148 | ) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /img/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxgribov/PopUp/84353a537969466aef6d86d906c00031b2626e11/img/example.gif --------------------------------------------------------------------------------