Toggle Sidebar in Code using SwiftUI NavigationView on iPad - swift

I'm trying to utilize the built-in sidebar from SwiftUI 2.0 by using NavigationView like this:
NavigationView {
MainView()
ListView()
DetailView()
}.navigationBarHidden(true)
But since I want to use my own Custom Back Button, I've hidden the NavigationBar and tried to toggle the sidebar with code which doesn't work.
self.presentationMode.wrappedValue.dismiss()
I've already seen a lot of solutions for macOS:
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
But I can't seem to find equivalent for iPad, thanks in advance.

I used this code to change the default sidebar settings:
extension UISplitViewController {
open override func viewDidLoad() {
super.viewDidLoad()
self.preferredDisplayMode = .secondaryOnly
self.preferredSplitBehavior = .overlay
}
}
self exposes several sidebar methods and properties that can be used. I hope it will be useful!

So this is not a good long term solution but if you are like me and 100% needed the native approach to work here's how it can be hacked. Using https://github.com/siteline/SwiftUI-Introspect you can find the right view controller in the hierarchy and set the display mode.
Text("Some View").introspectViewController { vc in
guard let splitVC = vc.parent?.parent as? UISplitViewController else {
return
}
splitVC.preferredDisplayMode = .oneBesideSecondary
}
This is BRITTLE but it works.

Related

Menu Bar Popover is missing from application's elements tree on macOS

I'm currently trying to write simple UI Tests for an App that comes with a popover in the macOS menu bar. One of the tests is supposed to open the menu bar popover and interact with its content. The problem is that the content seems to be completely absence from the application's element tree.
I'm creating the pop-up like so:
let view = MenuBarPopUp()
self.popover.animates = false
self.popover.behavior = .transient
self.popover.contentViewController = NSHostingController(rootView: view)
…and show/hide it on menu bar, click like this:
if let button = statusItem.button {
button.image = NSImage(named: NSImage.Name("MenuBarButtonImage"))
button.action = #selector(togglePopover(_:))
}
#objc func togglePopover(_ sender: AnyObject?) {
if self.popover.isShown {
popover.performClose(sender)
} else {
openToolbar()
}
}
func openToolbar() {
guard let button = menuBarItem.menuBarItem.button else { return }
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
NSApp.activate(ignoringOtherApps: true)
}
When I dump the element tree, the popover is not present:
[…]
MenuBar, 0x7fef747219d0, {{1089.0, 1.0}, {34.0, 22.0}}
StatusItem, 0x7fef74721b00, {{1089.0, 1.0}, {34.0, 22.0}}
[…]
Everything works when I compile the app and click around, I just can't make it work when it comes to automated UI testing. Any ideas?
Okay, after spending a lot of time with it, this solved my problem.
First, I had to add the Popover to the app's accessibility children like so:
var accessibilityChildren = NSApp.accessibilityChildren() ?? [Any]()
accessibilityChildren.append(popover.contentViewController?.view as Any)
NSApp.setAccessibilityChildren(accessibilityChildren)
However, this didn't seem to solve my problem at first. I'm using an App Delegate in a SwiftUI application. After tinkering with it for quite some time, I figured out that the commands I've added in my App.swift didn't go too well with my changes to the accessibility children in the App Delegate. After removing the commands from the Window Group, everything worked as expected.
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
// .commands {
// CommandGroup(replacing: CommandGroupPlacement.appSettings) {
// Button("Preferences...") { showPreferences() }
// }
// }
}
}

SwiftUI - Display view on UIWindow

I'm trying to display a custom SwiftUI view similar to a Toast in Android.
My issue is that I would like to display this particular view above everything else, using the current UIWindow.
Currently, while working on static func displayToastAboveAll() located in my ToastView, this is how far i got
public struct ToastView: View {
static func displayToastAboveAll() {
let window = UIApplication.shared.windows.filter { $0.isKeyWindow }.first // window
let viewToShow = ToastView(my params) // my view to display
// This part I'm not sure of
let hostingController = UIHostingController(rootView: viewToShow)
window?.addSubview(hostingController.view)
}
public var body: some View {
// MyDesign
}
}
Any idea how should I use the window to put the ToastView at its proper place, and still being able to navigate within the app (and use the outlets) while having the view displayed ?
I managed to do what I wanted.
Basically, this code is working, but I had to remove some constraints from my SwiftUI view and add them with UIKit using the static func.
Also, I had to pass by a modifier (see below) and put ToastView init in private.
public struct ToastModifier: ViewModifier {
public func body(content: Content) -> some View {
content
}
}
extension View {
public func toast() -> some View {
ToastView.displayToastAboveAll()
return modifier(ToastModifier())
}
}
This is done to force the use of either modifier (SwiftUI, by doing .toast, just like you'd do .alert) or directly by calling the static func ToastView.displayToastAboveAll() (UIKit).
Indeed, I dont wont this Toast to be a part of the view, I want to trigger it like an alert.
Finally, special warning because passing ToastView into UIHostingViewController will mess with some of the animations.
I had to rewrite animations in UIKit in order to have a nice swipe & fade animation.

PHPickerViewController hide Search Bar and Navigation Bar

I've been trying to implement a photo selection feature in a new app. My current approach is to use a PHPickerViewController embedded in a UIViewControllerRepresentable to display in a swiftUI view.
This is my makeUIViewController function.
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = filter
configuration.selectionLimit = limit
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
It is inside a struct named PhotoPicker :
struct PhotoPicker: UIViewControllerRepresentable {
What I want to hide is this part :
Yes, all of that.
Let me explain myself, the PickerView is always presented, it is not a pop-up, so there is no need for a cancel button. As you can see there is no done button either. That's because only one image needs to be selected so what happens is when the user taps on an image, the event that a new image was selected is called immediately. Removing the need for user confirmation. Then concerning the search bar, I don't really want it, I just want the user to select a photo and finally the little switch between photos and albums isn't really necessary in my case either.
I've tried a lot of different ways, including trying to set options for the controller when it is created in makeUIViewController. These options were for example :
controller.navigationController?.setToolbarHidden(true, animated: false)
controller.navigationController?.setNavigationBarHidden(true, animated: false)
And I also tried invoking view modifier in my SwiftUI body :
PhotoPicker(filter: .images, limit: 1) { (results) in
}
.navigationBarTitle("")
.navigationBarHidden(true)
.statusBar(hidden: true)
.navigationBarBackButtonHidden(true)
But again, none of them seems to work. So that's why I'm asking here, because it seems I tried everything and nothing is working...

How to disable lazy loading in NSTabViewController?

I am designing a SwiftUI wrapper for NSTabViewController with the toolbar style. I want it to be a drop-in replacement for TabView. TabView uses a modifier tabItem(_:) to specify the tab name and icon. So I designed a similar modifier for my own ToolbarTabView:
extension View {
func toolbarTabItem(_ label: LocalizedStringKey, nsImage: NSImage? = nil, tooltip: LocalizedStringKey? = nil) -> some View {
self.preference(key: ToolbarTabItemPreferenceKey.self, value: ToolbarTabItemPreference(label: label, nsImage: nsImage, tooltip: tooltip))
}
}
I wrap each View in a NSHostingController and create a NSTabViewItem. Then I use onPreferenceChange to set the NSTabViewItem's label and image property. Finally, I have a NSViewControllerRepresentable to pass my array of NSTabViewItem to a NSTabViewController. This all works well except for the following issue.
By design NSTabViewController will only load its first tab. This loads the first NSHostingController which lays out the first View. That calls onPreferenceChange and sets the label for the first tab. However, the remaining tabs are not loaded and therefore the label remains unset.
I know that I can re-design my APIs to pass in the labels and images explicitly and that works, but then how does Apple implement their TabView? They must have the same issue with the views being lazy loaded because the macOS implementation of TabView looks like NSTabViewController.
I think a workaround would be to force all the tabs to load, which is the title of this question, but I am open to other ideas as well.
Reference:
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabView.swift
https://github.com/utmapp/UTM/blob/dev/Platform/macOS/ToolbarTabViewController.swift
Here is the dumb workaround I came up with
public class UTMTabViewController: NSTabViewController {
public override func viewDidAppear() {
super.viewDidAppear()
for i in self.tabViewItems.indices {
self.selectedTabViewItemIndex = i
}
self.selectedTabViewItemIndex = 0
}
}
Basically I force load every tab once the view appears. I really hope there's a better answer than this but I'll leave it here just in case.

Set segment equal width for SwiftUI Picker with SegmentedPickerStyle

Using the SegmentedPickerStyle style Picker could make the control looks like UISegmentedControl. But I wonder how to adjust the segment width in the picker. For examle, the picker in the image has a different width for text.
Is there a way to make the segments the same width in the SwiftUI?
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}.pickerStyle(SegmentedPickerStyle())
...For examle, the picker in the image has a different width for text.
In case you arrive here seeking for iOS SwiftUI SegmentedPickerStyle solution... I've found the iOS SwiftUI .pickerStyle(SegmentedPickerStyle()) will conform to global UISegmentedControl.appearance() settings, so I've used the following to successfully apportion the width of each segment:
UISegmentedControl.appearance().apportionsSegmentWidthsByContent = true
This is particularly useful if, for example, you want to support Dynamic Type fonts in your app, which can otherwise cause segments with longer names to blow out and get truncated. [aside: I also use this trick to change the SwiftUI segmented picker's font size! see https://stackoverflow.com/a/71834578/3936065]
This is default macOS NSSegmetedControl behavirour
#property NSSegmentDistribution segmentDistribution API_AVAILABLE(macos(10.13));
// Defaults to NSSegmentDistributionFill on 10.13, older systems will continue to behave similarly to NSSegmentDistributionFit
Update: here is workaround, based on finding NSSegmentedControl in run-time view hierarchy.
Disclaimer: Actually it is safe, ie. no crash in run-time, but can stop working in future returning to default behaviour.
So, the idea is to inject NSView via representable into view hierarchy above (!!) Picker, as
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}
.overlay(NSPickerConfigurator { // << here !!
$0.segmentDistribution = .fillEqually // change style !!
})
.pickerStyle(SegmentedPickerStyle())
and configurator itself
struct NSPickerConfigurator: NSViewRepresentable {
var configure: (NSSegmentedControl) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let holder = view.superview?.superview {
let subviews = holder.subviews
if let nsSegmented = subviews.first?.subviews.first as? NSSegmentedControl {
self.configure(nsSegmented)
}
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
Ah the reach down to AppKit method.
Very clever indeed.
However this is not working for me, Monteray 12.3
Went to debug further using Xcode's Visual Debugger and I can see the NSPickerConfigurator class in the view hierarchy but no NSSegmetedControl.
It appears as if apple is clearing up NSViews from the hierarchy.
Time to think pure swiftui.