PHPickerViewController hide Search Bar and Navigation Bar - swift

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

Related

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.

Swift UISplitViewController how to go from triple to double columns

I'm having a lot of trouble figuring out how to structure a UISplitViewController.
I want:
A sidebar in the primary view (always)
I want the 1st sidebar navigation item (animals) to show triple (sidebar, animal list, animal detail)
I want the 2nd sidebar navigation item (profile) to show double (sidebar, profile view)
I see other apps doing this (GitHub for example), but I've really got no idea how they're managing it. Resources are hard to find, and most tutorials I've seen just show one or the other column styles.
I'm mostly looking for answers on how to architecture this well, but any code would also be massively appreciated!
SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController(style: .tripleColumn)
window?.makeKeyAndVisible()
}
Root view controller
class ViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
SidebarViewController(),
AnimalsViewController(),
AnimalDetailViewController()
]
// Example attempt at removing the secondary view
setViewController(ProfileViewController(), for: .supplementary)
setViewController(nil, for: .secondary)
hide(.secondary)
}
}
Desired behaviour
Animals
Profile
Cheers!
There is no "official" way to do it but it is possible. As far as I can tell, one of the best ways so solve it is to have two instances of UISplitViewController in your root view controller and juggle between them when needed. Here is my approach (approximately):
Disclaimer: This code was consulted with Apple engineers during the last WWDC22 on UIKit Labs. They have confirmed that it is very unfortunate that they currently do not offer a convenient way of doing it, and that this approach is probably the best way to do it. Feedback was filed and its ID passed to the engineers so hopefully we get an official API in the iOS 17 :D
rdar://FB10140263
Step 1. Initialise the UISplitViewControllers
private lazy var doubleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(doubleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .doubleColumn))
private lazy var tripleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(tripleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .tripleColumn))
Step 2. Initialise your sidebar VC and two separate UINavigationControllers
I have found it to be the most reliable solution for swapping sidebar VC. With a single UINavigationController instance there was a bug that the sidebar would randomly not appear. Two instances solve this problem while still keeping a single SidebarVC with proper focus state and already laid out content.
// Sidebar is shared and swapped between two split views
private lazy var sideBar = YourSideBarViewController()
private lazy var doubleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
private lazy var tripleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
Step 3. Make a property to store currently displayed SVC
It will come in handy in the next step when toggling between the two instances.
private var current: UISplitViewController?
Step 4. Implement Toggling between two styles when needed
This function should be called every time you want to navigate to a different screen from sidebar.
private func toggleStyleIfNeeded(_ style: UISplitViewController.Style) {
switch style {
case .doubleColumn:
// skip if the desired column style is already set up
if current === doubleColumnSVC { return }
// reassign current
current = doubleColumnSVC
// here add doubleColumnSVC as child view controller
// here add doubleColumnSVC.view as subview
// swap the sidebar
doubleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove tripleColumnSVC from parent
// here remove tripleColumnSVC.view from superview
case .tripleColumn:
// skip if the desired column style is already set up
if current === tripleColumnSVC { return }
// reassign current
current = tripleColumnSVC
// here add tripleColumnSVC as child view controller
// here add tripleColumnSVC.view as subview
// swap the sidebar
tripleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove doubleColumnSVC from parent
// here remove doubleColumnSVC.view from superview
default:
return
}
// If you are using UITabBarController for your compact style, assign it here
current?.setViewController(tabBar, for: .compact)
}
In lines that start with "here add" you will need to write your own code. I have simplified the code sample to make it shorter.
Step 5. Enjoy your SVC with dynamic columns!
Now you are basically ready to go! With this simple helper method on your root VC (or whichever one that is handling the navigation and managing the SVCs) you will have all the power that you need to achieve what you wanted, which is a UISplitViewController with dynamic number of columns!
func setViewController(
_ viewController: UIViewController,
for column: UISplitViewController.Column,
style: UISplitViewController.Style
) {
toggleStyleIfNeeded(style)
current?.setViewController(viewController, for: column)
}
We are using this approach in production for a few months now and it works great. The app supports iOS, iPadOS and Mac Catalyst. There are some things like customising the status bar style and getting consistent sidebar button experience a bit tricky to work perfectly but with some adjustments and help from the UISplitViewControllerDelegate everything is possible.
Good luck!
P.S. If anyone have walked this path before and is able to share suggestions, please do! I would love to learn more on how one could improve this dynamic split view experience both for users and developers.
To switch from 3 to 2 columns you must simply reinitialise the UISplitViewController with two columns (UISplitViewController(style: .doubleColumn) and reassign it to the window.rootViewController.
When reinitialising the UISplitViewController, you can either assign existing view controller objects, to maintain the current state, or initialise new ones. In case you assign existing view controller objects, it's probably handy to store these in variables after creating them for the first time.

Toggle Sidebar in Code using SwiftUI NavigationView on iPad

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.

How to check if UIViewController is already being displayed?

I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}

navigationbar missing after using UISearchController

i have a UICollectionViewController where i am showing list of task which is working fine, recently i tried to implement a UISearchBar for my TaskController after implementing that, when i try to launch any new viewcontroller by clicking on row inside my TaskController the newly launched view controller does not have UINavigationBar so i cant move back to my task list again. see following TaskController with task list:
Image
in above screen shot there is a star icon when user click on that, I launch following view controller which have a navigation bar(note: I have click directly without filtering records thats why i can see the navigation bar here.). UIViewController with UINavigationBar
Image
this is what i get when i click on star icon after filtering data with search bar.
navigation bar gone missing here
so i can not go back to task list controller also when i change a tab from below and come back the view controller got destroyed and i get a black screen with tab bar.
following code i have used to implement search bar which have the problem please help me to figure it out.
let taskSearchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
//set taskSearchController
taskSearchController.searchResultsUpdater = self
taskSearchController.dimsBackgroundDuringPresentation = false
navigationItem.searchController = taskSearchController
getTaskList(){
}
}
following method gives the filtered data from tasklist
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text, !searchText.isEmpty else{
self.taskList = self.originalTaskist
collectionView?.reloadData()
return
}
taskList = originalTaskist.filter({ task -> Bool in
return task.name!.lowercased().contains(searchText.lowercased())
})
collectionView?.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// This prevents the search bar to make trouble on pushed view controllers
definesPresentationContext = true
//...
}
Put definesPresentationContext = true inside of your View Controller that shows the search bar (the UICollectionViewController in your case.
Unfortunately, the documentation doesn't explain very well why this is working. This blog post explains it a little better.