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