Custom Modal View is a lightweight SwiftUI library that lets you present completely customizable modal dialogs.
It's API is built similar to the one of native .sheet(...)
view modifier and its usage fits well into the existing SwiftUI apps.
Simply add URL of this repository into your package dependencies:
https://github.com/jankaltoun/CustomModalView
To use this library with Cocoapods, add the following line into your Podfile.
pod 'CustomModalView'
This library is best used on iOS and iPadOS but macOS works as well.
The code would compile for tvOS and watchOS as well but the UI paradigms there are different enough so that using modals does not really make sense.
Please refer to the Example project for working iOS and macOS example.
To create a modal view, use the .modal(...)
view modifier on any view.
You need to pass a binding to some Bool
or Identifiable?
property so that the modal knows when to appear and disappear. The behaviour is exactly the same as with .sheet(...)
view modifier.
The example below shows initialization with a binding to a Bool
property.
struct MainView: View {
@State var modalIsDisplayed = false
var body: some View {
Button(action: { self.modalIsDisplayed = true }) {
Text("Show modal")
}
.modal(isPresented: $modalIsDisplayed) {
Text("Hello world!")
.padding()
}
}
}
And the next example shows initialization with a binding to an Identifiable?
property.
struct ContentView: View {
enum SomethingIdentifiable: Int, Identifiable {
case llama = 1
var id: Int {
rawValue
}
}
@State var somethingIdentifiable: SomethingIdentifiable? = nil
var body: some View {
Button(action: { self.somethingIdentifiable = .llama }) {
Text("Show modal")
}
.modal(item: $somethingIdentifiable) { item in
Text("Alpacas are the best!")
.padding()
}
}
}
By default, tapping the area behind the modal dismisses it. This behaviour can be modified (see the Styling a modal section).
If you'd like to dismiss your modal using a button, you can use the .modalPresentationMode
key to get a binding to ModalPresentationMode
and call dismiss()
on it.
Such property of your view should be defined as @Environment(\.modalPresentationMode) var modalPresentationMode: Binding<ModalPresentationMode>
.
Doing so would look like the following example.
import CustomModalView
struct ContentView: View {
@State var modalIsDisplayed = false
var body: some View {
Button(action: { self.modalIsDisplayed = true }) {
Text("Show modal")
}
.modal(isPresented: $modalIsDisplayed) {
DetailView()
}
}
}
struct DetailView: View {
@Environment(\.modalPresentationMode) var modalPresentationMode: Binding<ModalPresentationMode>
var body: some View {
VStack(spacing: 32) {
Text("I'm a modal")
Button(action: {
self.modalPresentationMode.wrappedValue.dismiss()
}) {
Text("Dismiss")
}
}
.padding()
}
}
Another option would be to just pass the binding to the property your modal is bound to and setting it to false
(or nil
if your property is Identifiable?
).
This is a bit cumbersome, but it can be done quite easily.
struct ContentView: View {
@State var modalIsDisplayed = false
var body: some View {
Button(action: { self.modalIsDisplayed = true }) {
Text("Show modal")
}
.modal(isPresented: $modalIsDisplayed) {
DetailView(isDisplayed: $modalIsDisplayed)
}
}
}
struct DetailView: View {
@Binding var isDisplayed: Bool
var body: some View {
VStack(spacing: 32) {
Text("I'm a modal.")
.fixedSize(horizontal: false, vertical: true)
Button(action: { self.isDisplayed = false }) {
Text("Dismiss")
}
}
.padding()
}
init(isDisplayed: Binding<Bool>) {
self._isDisplayed = isDisplayed
}
}
You are free to style your modal as you wish and there are two tools at your disposal:
- Styling the view
- Styling the container
View styling can be used to modify the appearance of the modal contents. You can use all SwiftUI tools to do so.
For example - a modal defined like the one in the example below.
struct ContentView: View {
@State var modalIsDisplayed = false
var body: some View {
Button(action: { self.modalIsDisplayed = true }) {
Text("Show modal")
}
.modal(isPresented: $modalIsDisplayed) {
Text("I'm a fancy Hello world!")
.frame(width: 200, height: 100, alignment: .topLeading)
.padding()
.background(Color.red)
.foregroundColor(.white)
}
}
}
Will result in the following UI.
If you need to adjust the corner radius, background of the modal or basically anything else, you can create your own modal style.
Defining your own style is easy. First create a struct
conforming to ModalStyle
protocol and then implement the two required functions.
Using the two functions you can completely customize the appearance of both the background and modal views as well as the animation used when the modal appears and disappears.
struct DefaultModalStyle: ModalStyle {
let animation: Animation? = .easeInOut(duration: 0.5)
func makeBackground(configuration: ModalStyle.BackgroundConfiguration, isPresented: Binding<Bool>) -> some View {
configuration.background
.edgesIgnoringSafeArea(.all)
.foregroundColor(.black)
.opacity(0.3)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.zIndex(1000)
.onTapGesture {
isPresented.wrappedValue = false
}
}
func makeModal(configuration: ModalStyle.ModalContentConfiguration, isPresented: Binding<Bool>) -> some View {
configuration.content
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 16))
.zIndex(1001)
}
}
In fact, this is exactly how the DefaultModalStyle is implemented.
A fancy style may look like the example below.
import CustomModalView
struct FancyModalStyle: ModalStyle {
let animation: Animation? = .easeInOut(duration: 0.5)
func makeBackground(configuration: ModalStyle.BackgroundConfiguration, isPresented: Binding<Bool>) -> some View {
configuration.background
.edgesIgnoringSafeArea(.all)
.foregroundColor(.blue)
.opacity(0.3)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.zIndex(1000)
}
func makeModal(configuration: ModalStyle.ModalContentConfiguration, isPresented: Binding<Bool>) -> some View {
configuration.content
.background(Color.yellow)
.clipShape(RoundedRectangle(cornerRadius: 8))
.zIndex(1001)
}
}
struct ContentView: View {
@State var modalIsDisplayed = false
var body: some View {
Button(action: { self.modalIsDisplayed = true }) {
Text("Show modal")
}
.modal(isPresented: $modalIsDisplayed) {
Text("Hello world!")
.padding()
}
.modalStyle(FancyModalStyle())
}
}
The result of such style is demonstrated by the following screenshots.
Jan Kaltoun, [email protected]
Please do whatever you want with this code.
If you link to this repository or mention me as the author, it will make me happy!