I want to create a popup menu like this
and it has a clear full screen overlay, when touch the overlay, popup menu will dismiss.
I tried add an overlay to root view, and add a menu list view to it, hardcoding position and frame for it, align with navigationItem then create a EnvironmentObject to store the overlay's toggle.
After this, I arrived my goal, but I think it was kind of mechanical, so my question is, is there has a good way to do this? like just use view modifier, or another with less step?
This is my root view:
struct Root : View {
TabbedView {
NavigationView {
HomePage()
}
}.overlay(...) <-- add a overlay at root view.
}
struct HomePage : View {
var body: some View {
ZStack {
List {...}
}
.navigationBarTitle("Home")
.navigationBarItems(trailing:
Button(action: {
// show popup menu
}) {
Image(systemName: "plus.circle")
}
)
}
}
I think this answer can help you. there have showing popUp using zstack.
Possibly a duplicate of >
Present Modal fullscreem SwiftUI
Related
I'm learning SwiftUI and having trouble with closing Each Tab View Element.
My App shows photos from user's album by TabView with pageViewStyle one by one.
And What I want to make is user can click save button in each view, and when button is clicked, save that photo and close only that view while other photos are still displayed. So unless all photos are saved or discarded, if user clicks save button, TabView should automatically move to another one.
However, I don't know how to close only one Tab Element. I've tried to use dismiss() and dynamically changing vm.images element. Latter one actually works, but it displays awkward movement and it also requires quite messy code. How could I solve this issue?
Here is my code.
TabView {
ForEach(vm.images, id: \.self) { image in
TestView(image: image)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
struct TestView: View {
#ObservedObject var vm: TestviewModel
...
var body: some View {
VStack(spacing: 10) {
Image(...)
Spacer()
Button {
...
} label: {
Text("Save")
}
}
You need actually to remove saved image from the viewModel container, and UI will be updated automatically
literally
Button {
vm.images.removeAll { $0.id == image.id } // << here !!
} label: {
Text("Save")
}
You need to use the selection initializer of TabView in order to control what it displays. So replace TabView with:
TabView(selection: $selection)
Than add a new property: #State var selection: YourIdType = someDefaultValue, and in the Button action you set selection to whatever you want to display.
Also add .tag(TheIdTheViewWillUse) remember that whatever Id you use must be the same as your selection variable. I recommend you use Int for the simple use.
I've added onboarding steps to my app using SwiftUI instead of a UIPageViewController. The root-level view has several steps:
struct OnboardingView: View {
#EnvironmentObject private var onboardingModel: OnboardingModel
var body: some View {
NavigationView {
VStack {
FirstStep()
NavigationLink(destination: SecondStep(),
tag: "second-step",
selection: $onboardingModel.selectedStep) {
EmptyView()
}
NavigationLink(destination: ThirdStep(),
tag: "third-step",
selection: $onboardingModel.selectedStep) {
EmptyView()
}
// etc.
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
The OnboardingModel keeps track of the selectedStep, so that changes to it will trigger the corresponding NavigationLink's tag.
When the onboarding view starts up, the FirstStep animates in from the right, just like an ordinary push navigation. The remaining steps, however, simply replace the previous step without any animation. Adding an .animation(.easeIn) to each step does nothing. How can I make this look and feel like a UIPageViewController?
I should note that each step hides the navigation bar and title:
.navigationTitle(Text(""))
.navigationBarHidden(true)
but removing these lines doesn't restore any built-in animation that the NavigationView provides.
Click the screencap to see it in action. Note that only the first scene is animated in; the rest just appear, even though that is obscured a bit by the permissions dialogs.
I am trying to create a situation in a SwiftUI Mac app where when I click a Button inside a parent view, only the Button's action is trigger—not any of the tap gestures attached to its parent.
Here is a simple example that reproduces the problem:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 30){
Button("Button"){
print("button clicked")
}
.padding(5)
.background(Color.blue)
.buttonStyle(PlainButtonStyle())
}
.frame(width: 500, height: 500)
.background(Color.gray)
.padding(100)
.gesture(TapGesture(count: 2).onEnded {
//Double click (open message in popup)
print("double click")
})
.simultaneousGesture(TapGesture().onEnded {
if NSEvent.modifierFlags.contains(.command){
print("command click")
}else{
print("single click")
}
})
}
}
Clicking the button triggers both button clicked and single click.
If you comment out the buttonStyle...
//.buttonStyle(PlainButtonStyle())
It works how I want it to. Only button clicked is fired.
It doesn't seem to matter which button style I use, the behavior persists. I really need a custom button style on the child button in my situation, so how do I get around this?
If you replace your .simultaneousGesture with a regular .gesture it works for me – and still recognizes the outer single and double taps.
I have an array of objects (employees) that I am displaying in a navigationview. Each object has a boolean property called "Active". If the employee is active I want the navigationlink for that list item to work as it normally would but if the employee is not active then I want that list item to be disabled so that the navigationlink or any swipe actions do not work. This is my code:
NavigationView {
List {
CustomSearchBar(searchText: $searchText, searchCategory: $searchCategory)
ForEach(Employees) { person in
ZStack {
NavigationLink(destination: DisplayDetails().environmentObject(person)) {
ListItem().environmentObject(person)
}
}
.swipeActions(edge: .leading, content: {
Button {
Employees.toggleArchiveFlag(for: person.id)
} label: {
Label("Archive", systemImage: !person.archived ? "square.and.arrow.down" : "trash")
}
.tint(!person.archived ? .blue : .red)
})
.disabled(!person.active)
}
}
.navigationTitle("Current Employees")
.padding(.horizontal,-15) // remove the padding that the list adds to the screen
}
What ends up happing is that when the view initially loads everything is enabled regardless of each employee's active status. But if I click any of the navigationlinks to load the "DisplayDetails" detailed view and then return back to the main navigationview OR if I click on any of the searchbar toggles or use the searchbar to filter my list of people then the view updates and the correct people are disabled.
It is almost as if the statement ".disabled(!person.active)" is being called too late. If that is the case then where should I be calling it? I have tried moving that statement in the following places:
The closing bracket of the Zstack. But this does nothing
Right below the "ListItem().environmentObject(person)" statement but this still shows the same behavior as mentioned earlier and when the navigationlink is eventually disabled then the swipeactions are still enabled.
Any help at all would be appreciated!
Figured out that the issue was with the logic that set the person.active boolean value not with the presentation of the navigation view items. Thanks.
I am creating an App where the login / register part is inside a modal, which is shown if the user is not logged in.
The problem is, that the user can dismiss the modal by swiping it down...
Is it possible to prevent this?
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) { // This needs to be non-dismissible
LoginRegister()
}
}
Second example:
I am using a modal to ask for information. The user should not be able to quit this process except by dismissing the modal with save button. The user has to input information before the button works. Unfortunately the modal can be dismissed by swiping it down.
Is it possible to prevent this?
iOS 15 and later:
Use .interactiveDismissDisabled(true) on the sheet, that's all.
Prev iOS 15:
You can try to do this by using a highPriorityGesture. Of course the blue Rectangle is only for demonstration but you would have to use a view which is covering the whole screen.
struct ModalViewNoClose : View {
#Environment(\.presentationMode) var presentationMode
let gesture = DragGesture()
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 300, height: 600)
.highPriorityGesture(gesture)
.overlay(
VStack{
Button("Close") {
self.presentationMode.value.dismiss()
}.accentColor(.white)
Text("Modal")
.highPriorityGesture(gesture)
TextField("as", text: .constant("sdf"))
.highPriorityGesture(gesture)
} .highPriorityGesture(gesture)
)
.border(Color.green)
}
}
This is a common problem and a "code smell"... well not really code but a "design pattern smell" anyway.
The problem is that you are making your login process part of the rest of the app.
Instead of presenting the LoginRegister over the App you should really be showing either App or LoginRegister.
i.e. you should have some state object like userLoggedIn: Bool or something and depending on that value you should show either App or LoginRegister.
Just don't have both in the view hierarchy at the same time. That way your user won't be able to dismiss the view.
If you dont mind using Introspect:
import Introspect
#available(iOS 13, *)
extension View {
/// A Boolean value indicating whether the view controller enforces a modal behavior.
///
/// The default value of this property is `false`. When you set it to `true`, UIKit ignores events
/// outside the view controller's bounds and prevents the interactive dismissal of the
/// view controller while it is onscreen.
public func isModalInPresentation(_ value: Bool) -> some View {
introspectViewController {
$0.isModalInPresentation = value
}
}
}
Usage:
.sheet {
VStack {
...
}.isModalInPresentation(true)
}
iOS 15+
Starting from iOS 15 you can use interactiveDismissDisabled.
You just need to attach it to the sheet:
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) {
LoginRegister()
.interactiveDismissDisabled(true)
}
}
Regarding your second example, you can pass a variable to control when the sheet is disabled:
.interactiveDismissDisabled(!isAllInformationProvided)
You can find more information in the documentation.
theoretically this may help you (I didn't tryed it)
private var isDisplayedBind: Binding<Bool>{ Binding(get: { true }, set: { _ = $0 }) }
and usage:
content
.sheet(isPresented: isDisplayedBind) { some sheet }