I'm having an issue with navigation view and using a popup view to add a core data object. When I dismiss the view instead of going one view back it goes to the root view. This is frustrating especially when you are 5 views in and just want to go back one view.
I've tried many ways to dismiss the view. I have tried different ways of calling and dismissing the popup view including:
Dismissing view:
#Environment(\.presentationMode) var presentationModel
then
self.presentationMode.wrappedValue.dismiss()
Calling and Dismissing view:
#State var showAddObjectView = false
.navigationBarItems(trailing: Button(action: {
self.showAddObjectView.toggle()
}) {
Image(systemName: "plus").imageScale(.large).font(Font.system(.body)).padding(15).accentColor(Color("BlackWhite"))
}.sheet(isPresented: $showAddObjectView) {
AddMoodView(showAddMood: $showAddMood, person: person).environment(\.managedObjectContext, self.managedObjectContext)
})
Then in the sheet view:
#Binding var showAddObjectView: Bool
Then on the save button:
self.showAddMood = false
Related
Context
NB: The question does NOT pertain to iOS
I have a Mac app that shows an NSPopover. The content of that popover is an NSHostingView that displays a simple SwiftUI view:
struct PopoverView: View
{
#State private var buttonWidthScale: CGFloat = 1.0
var body: some View
{
Button {
...
} label: {
RoundedRectangle(cornerRadius: 6.0)
.fill(.blue)
.scaleEffect(CGSize(width: buttonWidthScale, height: 1))
.animation(.easeInOut(duration: 2.5).repeatForever(), value: buttonWidthScale)
.onAppear {
buttonWidthScale = 0.96
}
}
}
}
The goal is to have a blue rectangle that very subtly "pulses" its width. The above works just fine to do that.
The Problem
I assumed (quite stupidly) that SwiftUI is smart enough to suspend the animation when the popover closes and the view is no longer on screen. That is not the case. Once the view appears for the first time, the app will now consume 5-6% CPU forever. The app correctly uses 0% CPU before this NSPopover appears for the first time and the animation kicks off.
What I Need
The SwiftUI .onAppear() and .onDisappear() methods are poorly named. They should really be called .onInsertion() and .onRemoval(), because they are only called when the view is added/removed from the hierarchy. (The "on appear" and "on disappear" names have historical meaning from NSViewController and Apple should never have recycled those names for a different intent.) As such, I cannot use these methods to start/stop the animation. .onAppear() is ever called only once and .onDisappear() is never called at all.
This animation should run continuously whenever the view is ON-SCREEN and then stop when the view disappears. So I need a replacement for .onAppear() and .onDisappear() that.....actually do what they imply they do!
My current approach is very hacky. From the NSViewController that holds the NSHostingView, I do this:
extension PopoverController: NSPopoverDelegate
{
func popoverWillShow(_ notification: Notification)
{
hostingView.rootView.popoverDidAppear()
}
func popoverDidClose(_ notification: Notification)
{
hostingView.rootView.popoverDidDisappear()
}
}
Where popoverDidAppear() and popoverDidDisappear() are two functions I've added to the PopoverView that replace the animation completely, as appropriate. (You can get rid of a .repeatForever() animation by replacing it with a new animation that is finite.)
But...this CANNOT be the right way, can it? There MUST be a canonical SwiftUI solution here that I just don't know about? Surely the future of Apple UI frameworks cannot need AppKit's help just to know when a view is shown and not shown?
This approach works, but I don't know if it's the "correct" way:
1. Add a Published Property in AppKit
To the NSViewController that manages the NSHostingView, I added this:
final class PopoverController: NSViewController, NSPopoverDelegate
{
#Published var popoverIsVisible: Bool = false
func popoverWillShow(_ notification: Notification)
{
popoverIsVisible = true
}
func popoverDidClose(_ notification: Notification)
{
popoverIsVisible = false
}
}
2. Use Combine in SwiftUI
In my SwiftUI view, I then did this:
struct PopoverView: View
{
#ObservedObject var popoverController: PopoverController
#State private var buttonWidthScale: CGFloat = 1.0
var body: some View
{
Button {
...
} label: {
RoundedRectangle(cornerRadius: 6.0)
.fill(.blue)
.scaleEffect(CGSize(width: buttonWidthScale, height: 1))
.onReceive(popoverController.$popoverIsVisible.dropFirst()) { isVisible in
if isVisible
{
withAnimation(.easeInOut(duration: 2.5).repeatForever()) {
buttonWidthScale = 0.96
}
}
else
{
// Replacing the repeating animation with a non-repeating one eliminates all animations.
withAnimation(.linear(duration: 0.001)) {
buttonWidthScale = 1.0
}
}
}
}
}
}
This appears to resolve the issue: CPU usage drops back to 0% when the popover is closed and the SwiftUI view leaves screen. The animation works correctly whenever the view appears.
But, again, there must be a better way to do this, right? This is a bunch of tight coupling and extra work just to accomplish something that ought to be automatic: "Don't waste CPU cycles on animations if the views aren't even on screen." Surely I'm just missing a SwiftUI idiom or modifier that does that?
I am trying to hide the navigation bar in SwiftUI, able to hide the navigation bar but it disables user-interaction of my header button.
I use the below code to hide the navigation bar.
.navigationBarBackButtonHidden(true).navigationBarHidden(true).navigationBarTitle("")
The above code work in another view but not working in Dashboard.
When I move to another view and come back to the dashboard then the navigation bar hides properly.
I also try this
NavigationLink(destination: MainTabBarView().navigationBarBackButtonHidden(true).navigationBarHidden(true).navigationBarTitle(""), isActive: $isPushHome) but no luck.
You have also another options in the toolbar.
Please try the following code :
var body: some View {
NavigationView {
VStack{
Text("Content").font(.largeTitle)
Spacer()
}
.padding()
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {}, label: {
Text("Test")
})
}
})
.navigationBarBackButtonHidden(true)
}
}
I want to push a UIImagePickerViewController with a button in my parent view controller, but I want to display the image in a different view, not the parent view. I've tried
Pushing a new view and calling the image picker from a button there. However, because I have this view embedded inside a navigation view, I have a problem of 2 navigation bars. I can't hide navigation bar because then, I can't go back to the parent view.
Pushing a new view and calling the image picker directly (with no button). However, the image picker is not closing on its own and I can't go back to my parent view controller.
How about something like this:
Have an observed object to be the main object (your enviornmentObject).
Add a UIImage property to it or any property you want to share between views (after all that's the job of an environmentObject
Share the enviornmentObject
This is your class
class AppState: ObservableObject {
#Published var selectedImage: UIImage? = nil // default it to nil in case nothing is selected
}
This is your main view
struct ContentView: View {
#EnviornmentObject var appState: AppState
#State var presentModal: Bool = false
var body: some View {
VStack {
// Image can now easily be accessed by calling self.appState.selectedImage in any view that has #EnviornmentObject var appState: AppState
if(self.appState.selectedImage != nil) {
Image(uiImage: self.appState.selectedImage!)
} else {
// Image doesn't exist, add a placeholder
Text("No image selected")
}
Button("Show Modal") {
self.presentModal.toggle()
}
}.sheet(isPresented: self.$presentModal) {
ModalView(presentModal: self.$presentModal)
}
}
}
// Your ImagePicker view or any other view that will change the selected Image
struct ModalView: View {
#EnviornmentObject var appState: AppState
#Binding var presentModal: Bool = false
var body: some View {
// Your logic to pick image goes here, I will simulate a button click
Button("I will set an image") {
self.appState.selectedImage = UIImage(named: "test.jpg")
self.presentModal.toggle()
}
}
}
In your SceneDelegate (VERY IMPORTANT)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(AppState()) // <- The important part
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Your modal view (or your image picker view, really anything)
EDIT: I agree it should be presented as a modal; however, it is not 100% necessary. Presenting it as a modal or not this should work though.
As the docs for UIUmagePickerController will tell you, it must be presented modally, not pushed. You are also responsible for dismissing it via the delegate.
I want to edit objects using popovers in my macOS application. But for some reason the popover does not appear anymore, when it was closed the popover while editing a TextField. (see gif bellow)
Any ideas, why this is happening?
Code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
SubView()
SubView()
SubView()
}.padding()
}
}
struct SubView: View {
#State var showPopover = false
var body: some View {
VStack {
Text("Label")
}.onTapGesture {
self.showPopover = true
}
.popover(isPresented: $showPopover, arrowEdge: .trailing) {
Popover()
}
}
}
struct Popover: View {
#State var test: String = ""
var body: some View {
TextField("Text", text: $test)
}
}
It looks like it is not enough one event to resign editor first responder and close previous popover, so state of following popover is toggled, but new popover is not allowed, because previous is still on-screen.
The following workaround is possible (tested & works with Xcode 11.2)
}.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
self.showPopover = true // delay activating new popover
}
}
Also it is possible to consider design approach when there is only one popover bindable to models of different subviews (which seems to me more appropriate) and manageable by the one state.
I am new to iOS development, and am trying to learn storyboarding, Swift, and the new features of iOS 8 at the same time.
I have created a very simple storyboard that uses a Popover presentation segue to display another view. On the simulator, if I run this for an iPad, it works as expected. However, if I run it for an iPhone, instead of a popover, it displays a full-screen view, on top of the original view. This is fine; however, there is no way to dismiss it and go back to the original screen.
I have watched the WWDC 2014 video "228 A Look inside presentation controllers" and they can show a dismiss button if they build the user interface entirely with code.
I have also watched the "411 What's new in interface builder" session, where they say that this can be done in Interface Builder, but they do not show it, promising to show how to do it in the lab, if anyone is interested. Unfortunately, I did not attend WWDC 2014, or know anyone who has. My Google searches have not returned anything helpful either.
You could add the navigation controller like this-
Set your popover view controller as the root view controller to a navigation controller.
Delete the popover segue that you are currently using
Reconnect the segue from the button you are displaying the popover from to the navigation controller.
On iPad you will get a popover and on iPhone you will get a modal presentation. Both the iPad and iPhone will show the navigation controller. Depending on your use case this may or may not be something you want. Here's a screen show of what the storyboard should look like.
If you really do want your view controller to always be a popover leave your storyboard the way it is and add something like this to your view controller that presents the popover-
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Your segue name"]) {
UIViewController *yourViewController = segue.destinationViewController;
yourViewController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popoverPresentationController = yourViewController.popoverPresentationController;
popoverPresentationController.delegate = self;
}
}
The view controller presenting the popover will also need to respond to this UIPopoverPresentationDelegate method
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
return UIModalPresentationNone;//always popover.
}
Lastly, you could do the following to only add the navigation controller to the modal presentation of your view controller on the iPhone and leave the popover on iPad without a navigation controller.
Leave your storyboard as is.
The proper place to inject the navigation controller is in - (UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style. In order for this to be called we must set ourselves as the delegate of the UIPopoverPresentationController.
Once again we will do this in prepareForSegue:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Your segue name"]) {
UIViewController *yourViewController = segue.destinationViewController;
yourViewController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popoverPresentationController = yourViewController.popoverPresentationController;
popoverPresentationController.delegate = self;
}
}
Then we will do this in the delegate method that I mentioned above
-(UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style
{
UIViewController *presentedViewController = controller.presentedViewController;
UINavigationController *navigationController = [[UINavigationController alloc]
initWithRootViewController:presentedViewController];
UIBarButtonItem *dismissButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonItemStyleDone target:self action:#selector(done:)];
presentedViewController.navigationItem.rightBarButtonItem = dismissButton;
return navigationController;
}
Good Luck!
If what you want is a popover on your iPad but a modal sheet with a close button on your iPhone then you can do it without creating an extra navigation controller in storyboard for the popover.
In Xcode 6.3 storyboard, you simply hook up a view controller and designate the segue as a "Present as Popover"
The code below should go in the view controller that segues to the popover, not in the popover itself:
First you set up the popover delegate:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "myPopoverSegueName") {
let vc = segue.destinationViewController
vc.popoverPresentationController?.delegate = self
return
}
}
Then you add the delegate extension (below your view controller's code) and create the navigation controller / close button on the fly:
extension myViewController: UIPopoverPresentationControllerDelegate {
func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
let btnDone = UIBarButtonItem(title: "Done", style: .Done, target: self, action: "dismiss")
let nav = UINavigationController(rootViewController: controller.presentedViewController)
nav.topViewController.navigationItem.leftBarButtonItem = btnDone
return nav
}
}
Then you add your dismiss function and you should be good to go:
func dismiss() {
self.dismissViewControllerAnimated(true, completion: nil)
}
I am not sure why you need to do storyboard setup for the Done button, all the work can be done programmatically with few lines of code. The important part is to implement some UIAdaptivePresentationControllerDelegate protocol methods exactly like below:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle
{
return .FullScreen
}
func presentationController(controller: UIPresentationController,
viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?{
var navController:UINavigationController = UINavigationController(rootViewController: controller.presentedViewController)
controller.presentedViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action:"done")
return navController
}
Then, a simple method to implement the dismissing behavior for the popover in case it was presented in full screen:
func done (){
presentedViewController?.dismissViewControllerAnimated(true, completion: nil)
}
and you done!
In my case, I had a small popup that I wanted to be a popup on both an iPhone and iPad - and wanted to avoid using a navigation bar with a Dismiss. Discovered that one needed to implement two delegate calls (Swift 3.0):
extension MyViewController : UIPopoverPresentationControllerDelegate {
// Needed for iPhone popup
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
// Needed for iPhone in landscape
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}
Its possible to do it with mimimal code whilst putting the logic into the storyboard instead. In the view controller that presents the popover, just put in the marker method
#IBAction func unwindToContainerVC(segue: UIStoryboardSegue) {
}
It does not need any code but needs to be present so you can control drag to the Exit icon later on when you use interface builder.
I have my popover content not take up the entire background view but have a small margin around it. This means you can use interface builder to create a tap gesture recogniser for this view. Control drag the gesture recogniser to the Exit icon which then pops up some Exit choices, one of which is the unwindToContainerVC method as seen above.
Now any tap around the edge (such as in an iPhone 4S scenario) takes you back to the presenting view controller.
Here is the connections inspector for the gesture recogniser: