I have custom ViewModifier that has some properties I would like to access from UIHostingController.
MyViewModifier is added to the root view of MyView which is rootView for UIHostingController:
struct MyViewModifier: ViewModifier {
var property1: Bool = true
var property2: String = ""
func body(content: Content) -> some View {
... modify content based on properties ...
}
}
extension View {
func myModifier(property1: Bool, property2: String) -> some View {
return modifier(MyViewModifier(property1: property1, property2: property2))
}
}
struct MyView: View {
var body: some View {
VStack { ... some content ... }
.myModifier(property1: true, property2: "Hello, world")
}
}
I have a UIHostingController subclass that handles some other stuff not related to this question, so I already have overloaded init.
Is it possible to check if the rootView passed to this HostingController has MyModifier and what its properties are?
Related
I'm writing my app using SwiftUI and VIPER. And to save the idea of viper(testability, protocols and etc) and SwiftUI reactivity I want to add 1 more layer - ViewModel. My presenter will ask data from interactor and will put in ViewModel, then view will just read this value.I checked does method that put data into view model works - and yes it does. But my view just don't see the property of view model (shows empty list) even if it conforms to ObservableObject and property is marked with Published. What is more interesting that if I store data in presenter and also mark it with published and observable object it will work. Thank in advance!
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
var interactor: BeersListInteractorProtocol
#ObservedObject var viewModel = BeersListViewModel()
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at page: Int){
interactor.loadList(at: page) { beers in
DispatchQueue.main.async {
self.viewModel.beers.append(contentsOf: beers)
print(self.viewModel.beers)
}
}
}
class BeersListViewModel:ObservableObject{
#Published var beers = [Beer]()
}
struct BeersListView: View{
var presenter : BeersListPresenterProtocol
#StateObject var viewModel : BeersListViewModel
var body: some View {
NavigationView{
List{
ForEach(viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(presenter.formattedABV(beer.abv))")
.font(.subheadline)
}
Some things to note.
You can't chain ObservableObjects so #ObservedObject var viewModel = BeersListViewModel() inside the class won't work.
The second you have 2 ViewModels one in the View and one in the Presenter you have to pick one. One will not know what the other is doing.
Below is how to get your code working
import SwiftUI
struct Beer: Identifiable{
var id: UUID = UUID()
var name: String = "Hops"
var abv: String = "H"
}
protocol BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer])->Void)
}
struct BeersListInteractor: BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer]) -> Void) {
completion([Beer(), Beer(), Beer()])
}
}
protocol BeersListPresenterProtocol: ObservableObject{
var interactor: BeersListInteractorProtocol { get set }
var viewModel : BeersListViewModel { get set }
func formattedABV(_ abv: String) -> String
func loadList(at page: Int)
}
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
var interactor: BeersListInteractorProtocol
//You can't chain `ObservedObject`s
#Published var viewModel = BeersListViewModel()
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at page: Int){
interactor.loadList(at: page) { beers in
DispatchQueue.main.async {
self.viewModel.beers.append(contentsOf: beers)
print(self.viewModel.beers)
}
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}
//Change to struct
struct BeersListViewModel{
var beers = [Beer]()
}
struct BeerListView<T: BeersListPresenterProtocol>: View{
//This is what will trigger view updates
#StateObject var presenter : T
//The viewModel is in the Presenter
//#StateObject var viewModel : BeersListViewModel
var body: some View {
NavigationView{
List{
Button("load list", action: {
presenter.loadList(at: 1)
})
ForEach(presenter.viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(presenter.formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}
Now I am not a VIPER expert by any means but I think you are mixing concepts. Mixing MVVM and VIPER.Because in VIPER the presenter Exists below the View/ViewModel, NOT at an equal level.
I found this tutorial a while ago. It is for UIKit but if we use an ObservableObject as a replacement for the UIViewController and the SwiftUI View serves as the storyboard.
It makes both the ViewModel that is an ObservableObject and the View that is a SwiftUI struct a single View layer in terms of VIPER.
You would get code that looks like this
protocol BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol { get set }
func formattedABV(_ abv: String) -> String
func loadList(at: Int, completion: ([Beer]) -> Void)
}
struct BeersListPresenter: BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at: Int, completion: ([Beer]) -> Void) {
interactor.loadList(at: at) { beers in
completion(beers)
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}
protocol BeersListViewProtocol: ObservableObject{
var presenter: BeersListPresenterProtocol { get set }
var beers: [Beer] { get set }
func loadList(at: Int)
func formattedABV(_ abv: String) -> String
}
class BeersListViewModel: BeersListViewProtocol{
#Published var presenter: BeersListPresenterProtocol
#Published var beers: [Beer] = []
init(presenter: BeersListPresenterProtocol){
self.presenter = presenter
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}
func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView<T: BeersListViewProtocol>: View{
#StateObject var viewModel : T
var body: some View {
NavigationView{
List{
Button("load list", action: {
viewModel.loadList(at: 1)
})
ForEach(viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(viewModel.formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(viewModel: BeersListViewModel(presenter: BeersListPresenter(interactor: BeersListInteractor())))
}
}
If you don't want to separate the VIPER View Layer into the ViewModel and the SwiftUI View you can opt to do something like the code below but it makes it harder to replace the UI and is generally not a good practice. Because you WON'T be able to call methods from the presenter when there are updates from the interactor.
struct BeerListView<T: BeersListPresenterProtocol>: View, BeersListViewProtocol{
var presenter: BeersListPresenterProtocol
#State var beers: [Beer] = []
var body: some View {
NavigationView{
List{
Button("load list", action: {
loadList(at: 1)
})
ForEach(beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}
func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView<BeersListPresenter>(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}
I have a swiftUI view with navigation links that when i click will navigate to another view . The issue is the second view navigationBa button title still has the title of the previous view instead of a logical back title . How i can have the title as Back with changing the title as "Back" in the first view .
First view navigationBar code: The second view just shows the news website in a WebView.
.navigationBarTitle("Breaking News")
The way i tried is changing the title to this:
.navigationBarTitle("Back")
This will work but the title of the first view changes to "Back" Instead of "Breaking News"
Is there any way i can fix this
An alternative approach is to hide the back button and create your own back button like this:
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination : SomeView()) {
Text("Open")
}
.navigationBarTitle("Breaking News")
}
}
}
// Use navigationBarItems for creating your own bar item.
struct SomeView : View {
#Environment(\.presentationMode) var mode
var body : some View {
Text("Hello, World!")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading:
Button(action : {
self.mode.wrappedValue.dismiss()
}){
Text("\(Image(systemName: "chevron.left"))Back")
})
}
}
The accepted answer looks glitchy, it removes a lot of standard behaviour and animations, including long press gesture.
Consider using custom backBarButtonTitle modifier:
struct FirstView: View {
var body: some View {
Text("First view")
.backBarButtonTitle("Back")
}
}
It sets the backButtonTitle property to the topmost UINavigationItem in stack. Be sure to use this modifier on the view, whose title you want to change, see documentation.
Here is the implementation of the backBarButtonTitle:
import SwiftUI
import UIKit
extension View {
func backBarButtonTitle(_ title: String) -> some View {
modifier(BackButtonModifier(title: title))
}
}
// MARK: - BackButtonModifier
struct BackButtonModifier: ViewModifier {
let title: String
func body(content: Content) -> some View {
content.background(BackButtonTitleView(title: title))
}
}
// MARK: - BackButtonTitleView
private struct BackButtonTitleView: UIViewRepresentable {
let title: String
func makeUIView(context _: Context) -> BackButtonTitleUIView {
BackButtonTitleUIView(title: title)
}
func updateUIView(_: BackButtonTitleUIView, context _: Context) {}
}
// MARK: - BackButtonTitleUIView
private final class BackButtonTitleUIView: UIView {
// MARK: Lifecycle
init(title: String) {
self.title = title
super.init(frame: .zero)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
override func layoutSubviews() {
super.layoutSubviews()
if didConfigureTitle {
return
}
let topNavigationItem = searchNavigationController(currentResponder: self)?
.topViewController?
.navigationItem
if let topNavigationItem {
topNavigationItem.backButtonTitle = title
didConfigureTitle = true
}
}
// MARK: Private
private let title: String
private var didConfigureTitle = false
private func searchNavigationController(currentResponder: UIResponder) -> UINavigationController? {
if let navigationController = currentResponder as? UINavigationController {
return navigationController
} else if let nextResponder = currentResponder.next {
return searchNavigationController(currentResponder: nextResponder)
} else {
return nil
}
}
}
I have a UIHostingController contained in a UIViewControllerRepresentable that holds a reference to a binding. When the binding changes, updateUIViewController is called, but the underlying view is not automatically re-rendered. How can I signal to the embedded UIHostingController that it needs to update its content?
Following is a simplified version of the scenario. Note that when the stepper changes, the first Text automatically updates, but the text contained within the PassthroughView UIViewControllerRepresentable does not automatically see its content get re-rendered.
import SwiftUI
struct ContentView: View {
#State var number = 99
var body: some View {
VStack {
Stepper("Stepper", value: $number)
Text("Direct Value: \(number)")
PassthroughView {
Text("Passthrough Value: \(number)")
}
Spacer()
}.font(.headline)
}
}
struct PassthroughView<V: View> : UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<V>
let content: V
init(#ViewBuilder _ content: () -> V) {
self.content = content()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PassthroughView<V>>) -> UIViewControllerType {
UIViewControllerType(rootView: content)
}
func updateUIViewController(_ controller: UIViewControllerType, context: UIViewControllerRepresentableContext<PassthroughView<V>>) {
// this is called when the binding changes;
// how to tell the UIHostingController to re-render?
}
}
The following code will work as desired:
I am not sure if it is good practice since I am not very familiar with UIKit.
struct PassthroughView<V: View> : UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<V>
let content: V
init(#ViewBuilder _ content: () -> V) {
self.content = content()
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PassthroughView<V>>) -> UIViewControllerType {
UIViewControllerType(rootView: content)
}
func updateUIViewController(_ controller: UIViewControllerType, context: UIViewControllerRepresentableContext<PassthroughView<V>>) {
// this is called when the binding changes;
// how to tell the UIHostingController to re-render?
controller.rootView = content
}
}
struct ContentView: View {
#State var number = 99
var body: some View {
VStack {
Stepper("Stepper", value: $number)
Text("Direct Value: \(number)")
PassthroughView {
Text("Passthrough Value: \(number)")
}
Spacer()
}.font(.headline)
}
}
I hope this helps!
I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?
I declare a QuizData class:
class QuizData: ObservableObject {
var allQuizQuestion: [QuizView] = [QuizView]()
let objectWillChange = PassthroughSubject<QuizData,Never>()
var currentQuestion: Int = 0 {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
and I use it there :
struct Quiz: View {
var continent: Continent
#EnvironmentObject var quizData: QuizData
var body: some View {
VStack
{
generateQuiz(continent: continent, quizData: self.quizData)
quizData.allQuizQuestion[quizData.currentQuestion]
}
.navigationBarTitle (Text(continent.name), displayMode: .inline)
}
}
The func generateQuiz is:
func generateQuiz(continent: Continent, quizData: QuizData) -> Void {
var capital: [Capital]
var alreadyUse: [Int]
for country in CountryData {
if country.continentId == continent.id
{
alreadyUse = [Int]()
capital = [Capital]()
capital.append(CapitalData[country.id])
for _ in 1...3 {
var index = Int.random(in: 1 ... CapitalData.count - 1)
while alreadyUse.contains(index) {
index = Int.random(in: 1 ... CapitalData.count - 1)
}
capital.append(CapitalData[index])
}
capital.shuffle()
quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
}
}
quizData.allQuizQuestion.shuffle()
}
I need to generate quiz question before the view appear. How should I do this?
First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a #ViewBuilder closure:
#functionBuilder
struct ViewBuilder {
// Build a value from an empty closure, resulting in an
// empty view in this case:
func buildBlock() -> EmptyView {
return EmptyView()
}
// Build a single view from a closure that contains a single
// view expression:
func buildBlock<V: View>(_ view: V) -> some View {
return view
}
// Build a combining TupleView from a closure that contains
// two view expressions:
func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
return TupleView((viewA, viewB))
}
// And so on, and so forth.
...
}
It's a Swift 5.1 feature that lets you do things like these:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:
struct ContentView: View {
private var values: [Int]! = nil
init() {
values = foo()
}
var body: some View {
List(values, id: \.self) { val in
Text("\(val)")
}
}
func foo() -> [Int] {
[0, 1, 2]
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Using the struct init and calling the function at the view creation.
EDIT: To answer your comment here below and since you are using an #EnvironmentObject you can do:
class ContentViewModel: ObservableObject {
#Published var values: [Int]!
init() {
values = generateValues()
}
private func generateValues() -> [Int] {
[0, 1, 2]
}
}
struct ContentView: View {
#EnvironmentObject var contentViewModel: ContentViewModel
var body: some View {
List(contentViewModel.values, id: \.self) { val in
Text("\(val)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ContentViewModel()) //don't forget this
}
}
#endif
And in your SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(ContentViewModel()) //don't forget this
)
self.window = window
window.makeKeyAndVisible()
}
}
This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.
I am trying to implement a button that presents another scene with a "Slide from Botton" animation.
PresentationButton looked like a good candidate, so I gave it a try:
import SwiftUI
struct ContentView : View {
var body: some View {
NavigationView {
PresentationButton(destination: Green().frame(width: 1000.0)) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewDevice("iPhone X")
.colorScheme(.dark)
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)"
)
}
}
}
#endif
And here is the result:
I want the green view to cover the whole screen, and also the modal to be not "draggable to close".
Is it possible to add modifier to PresentationButton to make it full screen, and not draggable?
I have also tried a Navigation Button, but:
- It doesn't "slide from bottom"
- It creates a "back button" on detail view, which I don't want
thanks!
Unfortunately, as of Beta 2 Beta 3, this is not possible in pure SwiftUI. You can see that Modal has no parameters for anything like UIModalPresentationStyle.fullScreen. Likewise for PresentationButton.
I suggest filing a radar.
The nearest you can currently do is something like:
#State var showModal: Bool = false
var body: some View {
NavigationView {
Button(action: {
self.showModal = true
}) {
Text("Tap me!")
}
}
.navigationBarTitle(Text("Navigation!"))
.overlay(self.showModal ? Color.green : nil)
}
Of course, from there you can add whatever transition you like in the overlay.
Although my other answer is currently correct, people probably want to be able to do this now. We can use the Environment to pass a view controller to children. Gist here
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: UIViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
Add an extension to UIViewController
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value: toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
And whenever we need it, use it:
struct MyView: View {
#Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
[EDIT] Although it would be preferable to do something like #Environment(\.viewController) var viewController: UIViewController? this leads to a retain cycle. Therefore, you need to use the holder.
Xcode 12.0 - SwiftUI 2 - iOS 14
Now possible. Use fullScreenCover() modifier.
var body: some View {
Button("Present!") {
self.isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
Hacking With Swift
This version fixes the compile error present in XCode 11.1 as well as ensures that controller is presented in the style that is passed in.
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
self.present(toPresent, animated: true, completion: nil)
}
}
To use this version, the code is unchanged from the previous version.
struct MyView: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
Button(action: {
self.viewController?.present(style: .fullScreen) {
MyView()
}
}) {
Text("Present me!")
}
}
}
My solution for this (which you can easily extend to allow other params on the presented sheets to be tweaked) is to just subclass UIHostingController
//HSHostingController.swift
import Foundation
import SwiftUI
class HSHostingControllerParams {
static var nextModalPresentationStyle:UIModalPresentationStyle?
}
class HSHostingController<Content> : UIHostingController<Content> where Content : View {
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let nextStyle = HSHostingControllerParams.nextModalPresentationStyle {
viewControllerToPresent.modalPresentationStyle = nextStyle
HSHostingControllerParams.nextModalPresentationStyle = nil
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
use HSHostingController instead of UIHostingController in your scene delegate
like so:
// Use a HSHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//This is the only change from the standard boilerplate
window.rootViewController = HSHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
then just tell the HSHostingControllerParams class what presentation style you want before triggering a sheet
.navigationBarItems(trailing:
HStack {
Button("About") {
HSHostingControllerParams.nextModalPresentationStyle = .fullScreen
self.showMenuSheet.toggle()
}
}
)
Passing the params via the class singleton feels a little 'dirty', but in practice - you would have to create a pretty obscure scenario for this not to work as expected.
You could mess around with environment variables and the like (as other answers have done) - but to me, the added complication isn't worth the purity.
update: see this gist for extended solution with additional capabilities
So I was struggling with that and I didn't like the overlay feature nor the ViewController wrapped version since it gave me some memory bug and I am very new to iOS and only know SwiftUI and no UIKit.
I developed credits the following with just SwiftUI which is probably what an overlay does but for my purposes it is much more flexible:
struct FullscreenModalView<Presenting, Content>: View where Presenting: View, Content: View {
#Binding var isShowing: Bool
let parent: () -> Presenting
let content: () -> Content
#inlinable public init(isShowing: Binding<Bool>, parent: #escaping () -> Presenting, #ViewBuilder content: #escaping () -> Content) {
self._isShowing = isShowing
self.parent = parent
self.content = content
}
var body: some View {
GeometryReader { geometry in
ZStack {
self.parent().zIndex(0)
if self.$isShowing.wrappedValue {
self.content()
.background(Color.primary.colorInvert())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height)
.transition(.move(edge: .bottom))
.zIndex(1)
}
}
}
}
}
Adding an extension to View:
extension View {
func modal<Content>(isShowing: Binding<Bool>, #ViewBuilder content: #escaping () -> Content) -> some View where Content: View {
FullscreenModalView(isShowing: isShowing, parent: { self }, content: content)
}
}
Usage:
Use a custom view and pass the showModal variable as a Binding<Bool> to dismiss the modal from the view itself.
struct ContentView : View {
#State private var showModal: Bool = false
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.showModal.toggle()
}
}, label: {
HStack{
Image(systemName: "eye.fill")
Text("Calibrate")
}
.frame(width: 220, height: 120)
})
}
.modal(isShowing: self.$showModal, content: {
Text("Hallo")
})
}
}
I hope this helps!
Greetings krjw