NSStatusBarButton with a NSHostingView autosize - swiftui

I have a NSStatusBarButton that contains a NSHostingView containing a Swift UI View.
I'd like that my Button and his subview to be the size of the Swift UI view content size.
The code looks like that:
// CustomSwiftUIView
import SwiftUI
struct CustomSwiftUIView: View {
var body: some View {
Text("Hello world, I have a dynamic width :)")
}
}
// AppDelegate
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
let mySwiftUIView = CustomSwiftUIView()
let innerView = NSHostingView(rootView: mySwiftUIView);
button.addSubview(innerView)
}
}
}
If I don't explicitly set a width and an height to the Button AND to the innerView, the button size is 0,0 and nothing is displayed.
Any idea how I can achieve the desired behavior?
Thanks!

Related

Why the List does not appear?

Why the presetsList does not appear? No errors were thrown though.
import SwiftUI
struct AddMessagePreset: View {
let presetsList = [
Preset(name: "preset text 1"),
Preset(name: "preset text 2"),
Preset(name: "preset text 3")
]
var body: some View {
List(presetsList) { singlePresetModel in
SinglePresetChild (presetModel: singlePresetModel)
}
}
}
import SwiftUI
struct Preset: Identifiable {
let id = UUID()
let name: String
}
struct SinglePresetChild: View {
var presetModel: Preset
var body: some View {
Text("Preset Name \(presetModel.name)")
}
}
UPDATE: To show a List inside another ScrollView (or List), you have to set a height on the inner list view:
struct Preview: View {
var body: some View {
ScrollView {
AddMessagePreset().frame(height: 200)
// more views ...
}
}
}
But let me advise against doing so. Having nested scroll areas can be very confusing for the user.
As discussed in the comments, your component code is fine. However, the way you integrate it into your app causes a problem. Apparently, nesting a List inside a ScrollView does not work properly (also see this thread).
List is already scrollable vertically, so you won't need the additional ScrollView:
struct Preview: View {
var body: some View {
VStack {
AddMessagePreset()
}
}
}
P.S.: If you only want to show AddMessagePreset and won't add another sibling view, you can remove the wrapping VStack; or even show AddMessagePreset as the main view, without any wrapper.

View resizing when hiding tab bar with Introspect in SwiftUI

I'm using Introspect to hide the tab bar on child navigation link pages. However, I've noticed some odd behavior when the app is backgrounded and then brought back to the foreground.
It seems like initially, the hidden tab bar is still taking up some space, but this disappears when cycling the app back to the foreground. I'm not sure if this is SwiftUI behavior or has to do with how I'm using Introspect / UIKit.
It's causing layout issues in my app, so I'd like to make the spacing consistent if possible.
Here's a minimal example that shows the behavior:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { tabBarController in
tabBarController.tabBar.isHidden = true
}
}
}
Here is the late answer. Basically add tabbar height to current view frame. And onDissappear restore view frame size
import SwiftUI
import Introspect
#State var uiTabarController: UITabBarController?
#State var tabBarFrame: CGRect?
struct ContentView: View {
var body: some View {
TabView {
VStack {
Spacer()
Text("Hello, world!")
}
}
.border(Color.red)
.introspectTabBarController { (UITabBarController) in
uiTabarController = UITabBarController
self.tabBarFrame = uiTabarController?.view.frame
uiTabarController?.tabBar.isHidden = true
uiTabarController?.view.frame = CGRect(x:0, y:0, width:tabBarFrame!.width, height:tabBarFrame!.height+UITabBarController.tabBar.frame.height);
}
.onDisappear {
if let frame = self.tabBarFrame {
self.uiTabarController?.tabBar.isHidden = false
uiTabarController?.view.frame = frame
}
}
}
}

SwiftUI changing text title of navigationBar Button at the current view to "Back" instead of inheriting the text from previous view title

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
}
}
}

Disable drag to dismiss in SwiftUI Modal

I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}

Present a View Non-Modally

I am creating a sign in page for my app and would like to present the home screen in a way that the user can not go back. In Swift UI how do I present it so the new view does not present in a card like style? I know this presenting style is now default for iOS 13.
This is what I already have.
import SwiftUI
struct Test : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: Extra() )
}
}
I would like the view to present full screen.
Use a NavigationView with a NavigationButton and hide the destination view's navigation bar's back button.
For example:
struct ContentView : View {
let destinationView = Text("Destination")
.navigationBarItem(title: Text("Destination View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Tap Here")
}
}
}
}
You can also disable the destination view's navigation bar altogether by doing let destinationView = Text("Destination").navigationBarHidden(true).