SwiftUI - Adding dimmer view behind custom alert - ios

I created a view modifier for reusable custom alert which works as expected. I now want to add a dimmer view in between presenting view and alert view i.e. view behind the alert which covers full screen and disables any clicks on the presenting view.
I tried adding background on the presenting view when alert is presented, but nothing is happening.
Custom alert view modifier, view extension and view model:
import Foundation
import SwiftUI
struct CustomAlertView: ViewModifier {
#Binding var isPresented: Bool
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}
func body(content: Content) -> some View {
content.overlay(alertContent())
}
#ViewBuilder
private func alertContent() -> some View {
GeometryReader { geometry in
if self.$isPresented.wrappedValue {
VStack {
Image(systemName: "info.circle").resizable().frame(width: 30.0, height: 30.0).padding(.top, 30).foregroundColor(.cyan)
Text("Error title").foregroundColor(Color.black).font(.title2).bold().lineLimit(nil).padding([.leading, .trailing], 24.0).padding(.top, 16.0)
Spacer()
Text("There was an error while processing your request.").foregroundColor(Color.black).font(.body).lineLimit(nil).padding([.leading, .trailing], 18.0).padding(.top, 16.0)
Spacer()
Button(action: { self.$isPresented.wrappedValue.toggle() }) {
Text("Ok").foregroundColor(.white).font(.largeTitle).bold()
}.padding(.bottom, 25.0)
}.fixedSize(horizontal: false, vertical: true)
.background(Color.purple)
.cornerRadius(10)
.clipped()
.padding([.leading, .trailing], 5.0)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.frame(width: 328.0)
}
}
}
}
extension View {
func customAlert(isPresented: Binding<Bool>) -> some View {
return modifier(CustomAlertView(isPresented: isPresented))
}
}
class CustomViewModel: ObservableObject {
#Published var showAlert = false
func doSomething() {
// Sets showAlert to true incase of network disconnect or some query failure.
self.showAlert = true
}
}
Content view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: CustomViewModel = CustomViewModel()
var body: some View {
VStack {
Spacer()
Button(action: { viewModel.doSomething() }) {
Text("Start").foregroundColor(Color.red).font(.title)
}.padding(.bottom, 100.0)
}
.background(viewModel.showAlert ? Color.gray : Color.clear)
.customAlert(isPresented: $viewModel.showAlert)
}
}
Here, ContentView is the presenting view since it's what is presenting the alert. I want to add a grayish sort of view/dimmer view covering full screen and it will be below the presented alert. When dimmer view is present and I click on "Start" button in ContentView, it should be disabled. I don't know if I can achieve this by modifying the custom alert view modifier, hence I was trying to add a background color to ContentView, but nothing seems to be happening. I have too much of code in the view model and content view, so I removed most of it and kept what I thought was needed.
How do I achieve it?

I was able to add a view behind the custom alert view modifier. Adding code here incase if someone comes looking for it in future.
I removed ".background(viewModel.showAlert ? Color.gray : Color.clear)" from my content view since I eventually wanted the logic to be part of custom alert view modifier and not add it to every view. In my customAlertView, I modified the body function as below:
func body(content: Content) -> some View {
content.overlay(self.$isPresented.wrappedValue ? Color.gray.ignoresSafeArea() : nil)
.overlay(self.$isPresented.wrappedValue ? alertContent() : nil)
}

Related

Create a common layout for the navigation bar in SwiftUI, so other SwiftUI views should reuse same Nav Bar

In iOS SwiftUI, how can we make a common layout for the navigation bar, so we can use that in all projects without rewriting the same code?
We can use ViewBuilder to create a base view for common code as follows:
struct BaseView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
// To-do: The most important part will go here
}
}
How can we add navigation bar code in View Builder or base view?
One way to achieve this is to use a custom view as an overlay.
For example, consider the below code which makes a custom navigation bar using an overlay:
struct Home: View {
var body: some View {
ScrollView {
// Your Content
}
.overlay {
ZStack {
Color.clear
.background(.ultraThinMaterial)
.blur(radius: 10)
Text("Navigation Bar")
.font(.largeTitle.weight(.bold))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 20)
}
.frame(height: 70)
.frame(maxHeight: .infinity, alignment: .top)
}
}
}
The ZStack inside the .overlay will make a view that looks like a navigation bar. You can then move it to its own struct view, add different variables to it and call it from the overlay.
You can create an extension of view like this way. You can check out my blog entry for details.
import SwiftUI
extension View {
/// CommonAppBar
public func appBar(title: String, backButtonAction: #escaping() -> Void) -> some View {
self
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
backButtonAction()
}) {
Image("ic-back") // set backbutton image here
.renderingMode(.template)
.foregroundColor(Colors.AppColors.GrayscaleGray2)
})
}
}
Now you can use this appBar in any place of the view.
struct TransactionsView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
VStack(spacing: 0) {
}
.appBar(title: "Atiar Talukdar") {
self.mode.wrappedValue.dismiss()
}
}
}
struct TransactionsView_Previews: PreviewProvider {
static var previews: some View {
TransactionsView()
}
}

Create a view over navigationView, SwiftUI

I'm creating a view embedded in a ZStack, something like this:
ZStack(alignment: .top) {
content
if self.show {
VStack {
HStack {
This is a viewModifier so I call this in my main view with for example: .showView().
But what happened is that if I have a NavigationView, this view is only showing below the navigationView. (I have a navigationViewTitle that is over my view).
How can I solve this problem? I was thinking about some zIndex but it is not working. I thought also about some better placement of this .showView(), but nothing to do.
Here is a demo of possible approach (it can be added animations/transitions, but it is out of topic). Demo prepared & tested with Xcode 11.4 / iOS 13.4
struct ShowViewModifier<Cover: View>: ViewModifier {
let show: Bool
let cover: () -> Cover
func body(content: Content) -> some View {
ZStack(alignment: .top) {
content
if self.show {
cover()
}
}
}
}
struct DemoView: View {
#State private var isPresented = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Link", destination: Button("Details")
{ self.isPresented.toggle() })
Text("Some content")
.navigationBarTitle("Demo")
Button("Toggle") { self.isPresented.toggle() }
}
}
.modifier(ShowViewModifier(show: isPresented) {
Rectangle().fill(Color.red)
.frame(height: 200)
})
}
}

The z-index of SwiftUI View and UIKit View

The SwiftUI scroll view is hiding some area of a view presented from UIViewControllerRepresentable viewController.
Part of the SwiftUI code, The GoogleMapsView is the UIViewControllerRepresentable viewController.
struct ContentView: View {
var body: some View {
ZStack(alignment: .top) {
GoogleMapsView()
VStack(alignment: .leading) {
Text("Top Locations near you")
ScrollView(.horizontal, showsIndicators: false) {
HStack() {
ForEach(dataSource.topPlaces) { place in
PlaceCardView(placeImage: place.image, placeName: place.name, placeRating: place.rating, placeRatingTotal: place.ratingTotal, topPlace: place)
}
}
.padding(.horizontal, 10)
}
.background(Color.clear)
.foregroundColor(.black)
.frame(height: 200)
.opacity(self.finishedFetching ? 1 : 0.1)
.animation(.easeInOut(duration: 1.3))
}.edgesIgnoringSafeArea(.all)
}
The view I want to put on the top is from the GoogleMapView() which I put it on the "bottom" of the ZStack because I want that Scroll view to flow on the map. However the view show on the map when marker is tapped is cover up by the SwiftUI ScrollView
I tried to change their zIndex of the scrollView, zPosition of the pop up view or bringSubviewToFront etc. None of them work.
You need to inject local state via binding into `` and change it there on popup shown/hidden.
Here is some pseudo code
struct ContentView: View {
#State private var isPopup = false
var body: some View {
ZStack(alignment: .top) {
GoogleMapsView(isPopup: $isPopup)
VStack(alignment: .leading) {
Text("Top Locations near you")
if !isPopup {
ScrollView(.horizontal, showsIndicators: false) {
// other code
}
// other code
}
}.edgesIgnoringSafeArea(.all)
}
struct GoogleMapsView : UIViewControllerRepresentable {
#Binding isPopup: Bool
// other code
// make self.isPopup = true before popup and false after

SwiftUI | How to override life cycle methods like viewWillAppear

In my existing app, there are few UI configurations like setting up a custom navigation bar, setting a full-screen background images and some more, on all view controllers. For this purpose, configurations are made in a base view controller in viewWillAppear, all view controllers and inherited from it. So configurations are made without doing anything in child view controllers
How to achieve a similar implementation using SwiftUI?
SwiftUI provides onAppear() & onDisappear() methods on View, which is Struct and doesn't support inheritance. If I make extensions methods or protocol, I have to call methods from all Views.
Any help is appreciated...
Make your own container view. You can use #ViewBuilder to make your view behave like VStack and HStack where it takes a content closure full of subviews.
For example, here's a custom container that gives the main content a pink background, and puts a toolbar under it:
import SwiftUI
struct CustomContainer<Content: View>: View {
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
let content: Content
var body: some View {
VStack(spacing: 0) {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(hue: 0, saturation: 0.3, brightness: 1))
Toolbar()
}
}
}
struct Toolbar: View {
var body: some View {
HStack {
Spacer()
button("person.3.fill")
Spacer()
button("printer.fill")
Spacer()
button("heart.fill")
Spacer()
button("bubble.left.fill")
Spacer()
} //
.frame(height: 44)
}
private func button(_ name: String) -> some View {
Image(systemName: name)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(4)
.frame(width: 40, height: 40)
}
}
And then here's a view that uses it:
struct HelloView: View {
var body: some View {
CustomContainer {
VStack {
Text("hello")
Text("khawar")
}
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(HelloView())
Result:

SwiftUI add subview dynamically but the animation doesn't work

I would like to create a view in SwiftUI that add a subview dynamically and with animation.
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
if isButtonVisible {
AnyView(DetailView())
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}else{
AnyView(Text("test"))
}
}
}
}
The above code works fine with the animation . however when i move the view selection part into a function, the animation is not working anymore (since i want to add different views dynamically, therefore, I put the logic in a function.)
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView().transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
func subView() -> some View {
if isButtonVisible {
return AnyView(DetailView())
}else{
return AnyView(Text("test"))
}
}
}
it looks totally the same to me, however, i don't understand why they have different result. Could somebody explain me why? and any better solutions? thanks alot!
Here's your code, modified so that it works:
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView()
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
}
func subView() -> some View {
Group {
if isButtonVisible {
DetailView()
} else {
Text("test")
}
}
}
}
Note two things:
Your two examples above are different, which is why you get different results. The first applies a transition and animation to a DetailView, then type-erases it with AnyView. The second type-erases a DetailView with AnyView, then applies a transition and animation.
Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.
If you wanted different animations on the two possibilities for your subview, you can now apply them directly to DetailView() or Text("test").
Update
The Group method will only work with if, elseif, and else statements. If you want to use a switch, you will have to wrap each branch in AnyView(). However, this breaks transitions/animations. Using switch and setting custom animations is currently not possible.
I was able to get it to work with a switch statement by wrapping the function that returns an AnyView in a VStack. I also had to give the AnyView an .id so SwiftUI can know when it changes. This is on Xcode 11.3 and iOS 13.3
struct EnumView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
view(for: viewModel.viewState)
.id(viewModel.viewState)
.transition(.opacity)
}
}
func view(for viewState: ViewModel.ViewState) -> AnyView {
switch viewState {
case .loading:
return AnyView(LoadingStateView(viewModel: self.viewModel))
case .dataLoaded:
return AnyView(LoadedStateView(viewModel: self.viewModel))
case let .error(error):
return AnyView(ErrorView(error: error, viewModel: viewModel))
}
}
}
Also for my example in the ViewModel I need to wrap the viewState changes in a withAnimation block
withAnimation {
self.viewState = .loading
}
In iOS 14 they added the possibility to use if let and switch statements in function builders. Maybe it helps for your issues:
https://www.hackingwithswift.com/articles/221/whats-new-in-swiftui-for-ios-14 (at the article's bottom)

Resources