Window Control Buttons Missing from New Window in SwiftUI App - swiftui

Starting with a new SwiftUI macOS project in Xcode, I set up some very basic functionality.
#main
struct SwiftUIWindowTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//ContentView
struct ContentView: View {
var body: some View {
//Button to open a new window
Button("Open Preferences"){
openPreferences()
}
.padding(100)
}
//Open the new window
func openPreferences(){
let preferencesWindow = NSWindow()
preferencesWindow.contentView = NSHostingView(rootView: PreferencesView())
let controller = NSWindowController(window: preferencesWindow)
controller.showWindow(nil)
}
}
//PreferencesView
struct PreferencesView: View {
var body: some View {
Text("Preferences")
.frame(width:300, height:200)
}
}
On app launch, I see what I expect:
But after I click Open Preferences, I see a new window appear like this:
Why are the control buttons (close, minimize, zoom) missing on that popup window?
I have tried explicitly setting the buttons to be visible (even though they should be by default) and nothing changes:
preferencesWindow.standardWindowButton(.closeButton)?.isHidden = false
preferencesWindow.standardWindowButton(.miniaturizeButton)?.isHidden = false
preferencesWindow.standardWindowButton(.zoomButton)?.isHidden = false
I have this same sample working fine in an AppKit app, so it appears to be something weird with SwiftUI. Any ideas?

Looks like NSWindow will create a window like that unless you set a hosting controller on it. This modified version of your code works for me:
func openPreferences(){
let preferencesWindow = NSWindow(contentViewController: NSHostingController(rootView: PreferencesView()))
let controller = NSWindowController(window: preferencesWindow)
controller.showWindow(nil)
}

Related

Animate an animated gif only while pressing the button

This is my code :
struct Animation: View {
var body: some View{
Image("Animation.gif")
}
}
i have a gif image and i want this image to only be animated once a button click only, i need help!
and if there is no button click then it won't be animated thank you
As far as I know, SwiftUI doesn't have anything that can do this. Instead, you should use an #State variable to track whether or not the button has been pressed. If it hasn't, show a non-animated image, and if it has, show a different image, but animated this time.
Something like this:
#State var buttonIsClicked = false
let nonAnimated = <static image>
let animated = <your gif>
var body: some View {
VStack {
if buttonIsClicked {
animated
} else {
nonAnimated
}
Button("Click to animate") {
buttonIsClicked.toggle()
}
}
}

NSStatusBarButton with a NSHostingView autosize

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!

How to reload the TabView in order to change TabBar background color without having User actions on it (eg: from a settings panel change)?

To change TabBar background and other properties you have about 2 ways to process.
via the init() of the view containing the TabView
(see: https://stackoverflow.com/a/56971573/2192483)
via an .onAppear() directly on the TabView
(see: https://stackoverflow.com/a/63414605/2192483 - thanks to #Asperi ;
see: https://schwiftyui.com/swiftui/customizing-your-tabviews-bar-in-swiftui/ - thanks to #Dan O'Leary)
These solutions are efficients at View load and if you reload Tab Bar by touching tabs.
But IT DOES'NT WORK, if you want to change a TabBar properties programmatically, without User interaction on the TabBar Buttons, through the regular way of properties defined in #Published values. This doesn't work because either though init() or .onAppear(), these 2 methods are not subscribers of prop. publishers so the view embedding the TabView doesn't reload.
I finally found a way to trigger the TabView reload not from user tab button touches, with regular #Published properties.
Here is example based on an app with a settings panel allowing to change app skin, including the TabBar background color.
1) The Main struct embed the ContentView that embed the TabView
• SettingsModel embed the properties that share a) the skin values b) a flag to trigger the reload
• ContentView owns one param: skinChanged
#main
struct MyApp: App {
#ObservedObject private var settings = SettingsModel.shared
var body: some Scene {
WindowGroup {
ContentView(skinChanged: settings.skinChanged)
}
}
}
.
2) The ContentView struct embed the TabView
• init(skinChanged: Bool) allows to custom TabBar appearance that
will reload if settings.skinChanged changes of value
• init / if !skinChanged { } applies TabBar appearance only if needed
• body / if !self.settings.skinChanged { } allows to create a change in
the view alternating from TabView to a Rectangle (as a background)
struct ContentView: View {
#ObservedObject private var settings = SettingsModel.shared
init(skinChanged: Bool) {
if !skinChanged {
let tabBarAppearance = UITabBar.appearance()
tabBarAppearance.backgroundColor = UIColor(settings.skin.tabBarBg)
tabBarAppearance.unselectedItemTintColor = UIColor(settings.skin.tabBarUnselected)
}
}
var body: some View {
if !self.settings.skinChanged {
TabView(selection: $someStateProp) {
...
}
} else {
Rectangle()
.fill(settings.skin.tabBarBg)
}
}
}
.
3) The SettingModel struct publishes properties that trigger skin changes and skin values
• In order to create a change in ContentView(skinChanged:) with
settings.skinChanged, the value must pass from false to true then
back to false
• To let the UI applies each changes the 2 changes must be
parallelized with 2 synchronized tasks
class SettingsModel: ObservableObject {
static public let shared = SettingsModel()
...
#Published var skin = Skin(style: .modern)
#Published var skinId = 0 {
didSet {
...
self.skin = Skin(style: Skin.Style.allCases[self.skinId])
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.skinChanged = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.skinChanged = false
}
}
}
#Published var skinChanged = false
...
}

How to fix a Picker navigation works only when I long press a link

I'm currently developing an application using SwiftUI.
When I use a onTapGesture at Form in NavigationView, navigation of Picker the inside doesn't work.
It works but only when I long-press a link like a LongPressGesture.
If I don't use onTapGesture, the navigation of Picker works, as usual, But in that case, I can not close a keyboard when I use TextField choosing numberPad as keyboardType...
How could I solve this problem?
Here is a code:
OnTapGestureTest.swift
import SwiftUI
struct OnTapGestureTest: View{
#State var inputNo:String = ""
#State var selectNo:Int = 0
var body: some View {
NavigationView{
Form{
TextField("InputNo", text: $inputNo)
.keyboardType(.numberPad)
Picker(selection:$selectNo, label: Text("SelectNo")){
Text("0").tag(0)
Text("1").tag(1)
Text("2").tag(2)
Text("3").tag(3)
Text("4").tag(4)
Text("5").tag(5)
}
}
.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
UPDATED
I tried to solve my question by referencing here.
*I want to close keyboard only on Tap outside (without handling drags) in iOS 13.0
SceneDelegate.swift
...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
...
let tapGesture = UITapGestureRecognizer(target: window, action:#selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self
window?.addGestureRecognizer(tapGesture)
}
...
But it still doesn't work well... Do I need to do some more things? misunderstand the answer?
Xcode: Version 12.0.1
iOS: 13.0
You could add the tap gesture globally, with the help of UIWindow:
extension UIApplication {
func addTapGestureRecognizer() {
guard let window = windows.first else { return }
let tapGesture = UITapGestureRecognizer(target: window, action: #selector(UIView.endEditing))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self
window.addGestureRecognizer(tapGesture)
}
}
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
In your main: (The idea: as soon as your app launches, this tapGesture is added to the window)
#main
struct YourApp: App {
...
var body: some Scene {
WindowGroup {
//Your initial view
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
}
This is especially convenient since you don't have to take care of hiding the keyboard ever again inside your views and it's not blocking any of your controls.
Note
This code is from Here
Also, I saw iOS13 - for non SwiftUI Cycle, this would be the equivalent: Here

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).