Because SwiftUI has such a stifling navigational system, I'm attempting to use pushViewController within my SwiftUI views.
However, when I run the code and press the button, I get the following error -
Fatal error: Unexpectedly found nil while unwrapping an Optional value:
file /Users/.../Conjugate/Conjugate/Pages/Practice/PracticeView.swift, line 93
Here is my code -
PracticeView.swift
...
Button(action: {
/* Line 93 */ UIApplication.shared.windows[0].rootViewController?.navigationController!.pushViewController(UIHostingController(rootView: ResultView()), animated: true)
}) { ... }
...
SceneDelegate.swift
...
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let nonEmbeddedViewController = UIHostingController(rootView: PracticeView(verb: verbData.shuffled(), numberOfQuestions: CGFloat(verbData.count)))
let navigationController = UINavigationController(rootViewController: nonEmbeddedViewController)
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}
}
...
Does anybody know how to fix this? I suspect that the navigationController has a value of nil when unwrapped, but I don't know the solution. Thank you!
EDIT - Clarification
I'm trying to make an educational app that quizzes you on a certain topic. The "practice view" is where the user answers questions, which get replaced every time they press the button in the bottom right corner (the one I mentioned in my question). However, when all the questions have been answered, the button needs to open another view (the "result view") instead of just switching the text in the current view. In addition, the navigation bar must be hidden in both the practice view and the result view, and modal sheets won't do. If you need a reference, I guess Duolingo or this slideshow could be useful.
Code markdown may help you for this. Ignoring the SwiftUI navigation view, let go with a model that need to show either (a) a practice view (with an array of questions) or (b) the results. If you set up your model with:
#Published var currentQuestion:Question
#Published var showResults:Bool
(This is very much pseudo-code!)
You can actually have a (simple) content view (again, pseudo-code) that is:
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
if model.showResults {
ShowResults()
} else {
ShowCurrentQuestion()
}
}
}
ShowResults and ShowCurrentQuestion are Views. You can do all kinds of animation (the default is to fade in/out) between the two, and there's absolutely no need for "navigation". As long as your model drives ContentView, it works.
And yes, I'm not addressing NavigationView - while I'm 'old school" with regards to UINavigationController, the SwiftUI app I'm working on doesn't need anything similar to push/pop or segues. BUT... I am using what I've just described.)
Related
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.
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.
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...
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.
What it does
When the first page in my tab bar controller loads, I retrieve data from a json file
I store it in an array (in the first view controller)
The data obtained will be displayed in the second view controller. The data is already loaded and stored in an array in the first view controller.
Problem:
I can't figure out a way to pass the data between the two view controllers. Can't pass data based on the segue identifier since it is a tab bar controller
Please help!
If you need to pass the data between view controllers then :
var secondTab = self.tabBarController?.viewControllers[1] as SecondViewController
secondTab.array = firstArray
I ended up using a singleton as Woodster suggested in his answer above.
In Swift 3
Create a new swift file and create a class:
class Items {
static let sharedInstance = Items()
var array = [String]()
}
In any of your view controllers you can access your array like this:
Items.sharedInstance.array.append("New String")
print(Items.sharedInstance.array)
H. Serdar's code example is right, that's the way to access another tab's view controller and give it data.
Keep in mind that when you pass an array in Swift, you're passing it by value, unlike Objective-C, which passes it by reference. This means that changes made by your second view controller won't be reflected in your first view controller, because your second one is using a copy of the array, not the same array. If you want both view controllers to modify the same array, put the array in a class, and pass a single instance of that class around.
Some other considerations:
You could subclass the TabBarController to give it a property that'll store your data, and that would be available to all tabs using:
if let tbc = tabBarController as? YourCustomTabBarSubclass {
println("here's my data \(tbc.array)")
}
In that situation, you'd be accessing the same array from multiple tabs, so changes in one tab would be reflected elsewhere.
I recommend against the approach of using your App Delegate as a centralized place to store data. That's not the purpose of the application's delegate. Its purpose is to handle delegate calls for the application object.
View Controllers should have all the data, encapsulated within them, that they need to do their job. They have a connection to their model data (such as your array, or a reference to a database or a managed object context) as opposed having a view controller reach out to another object by traversing a view controller graph or going into the delegate or even using a global variable. This modular, self contained construction of View Controllers lets you restructure your app for similar but unique designs on different devices, such as presenting a view controller in a popover on one device (like an iPad) and presenting it full screen on another, such as an iPhone.
SWIFT 3
In your first viewcontroller, declare your variable (in your case an array) like you normally would.
In your second viewcontroller, do this:
var yourVariable: YourVariableClass {
get {
return (self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable
}
set {
(self.tabBarController!.viewControllers![0] as! FirstViewControllerClass).yourVariable = newValue
}
}
This works because, in a tabbarcontroller all viewcontrollers behind the tab items are initialized. By doing this in your second viewcontroller you are actually getting/setting the variable from/in the first viewcontroller.
For Xcode 11 & Swift 5 + Storyboard + Dependency Injection Approach
Assuming you are using a storyboard this is a method I have devised.
Step 1:
Put an identifier on your tabBarController like I did in the image below.
Step 2:
In the scenedelegate.swift file (NOT appDelegate.swift), add the following code to the appropriate func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { method.
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).
self.window = self.window ?? UIWindow()//#JA- If this scene's self.window is nil then set a new UIWindow object to it.
//#Grab the storyboard and ensure that the tab bar controller is reinstantiated with the details below.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "tabBarController") as! UITabBarController
for child in tabBarController.viewControllers ?? [] {
if let top = child as? StateControllerProtocol {
print("State Controller Passed To:")
print(child.title!)
top.setState(state: stateController)
}
}
self.window!.rootViewController = tabBarController //Set the rootViewController to our modified version with the StateController instances
self.window!.makeKeyAndVisible()
print("Finished scene setting code")
guard let _ = (scene as? UIWindowScene) else { return }
}
You will notice that the scenedelgate.swift file has a member variable; var window: UIWindow?. This used to be part of appDelegate but was changed in xCode 11 and Swift 5 so a lot of similar answers and tutorials will be out of date.
The part in the code that says storyboard.instantiateViewController(withIdentifier: you will want to add the name you used for the parameter. In my screenshot you will see I called it tabBarController.
To make this function work on any type of viewController without having to instantiate each one separately on an index, I've used a protocol strategy called StateControllerProtocol. We will be creating this next along with the StateController which will hold the global variables.
Step 3:
In stateController.swift or whatever you want to name this file, add the following code removing aspects that do not apply to your project.
import Foundation
struct tdfvars{
var lateBED:Double = 0.0
var acuteBED:Double = 0.0
var rbe:Double = 1.4
var t1half:Double = 1.5
var alphaBetaLate:Double = 3.0
var alphaBetaAcute:Double = 10.0
var totalDose:Double = 6000.00
var dosePerFraction:Double = 200.0
var numOfFractions:Double = 30
var totalTime:Double = 168
var ldrDose:Double = 8500.0
}
//#JA - Protocol that view controllers should have that defines that it should have a function to setState
protocol StateControllerProtocol {
func setState(state: StateController)
}
class StateController {
var tdfvariables:tdfvars = tdfvars()
}
The variables you want to share between views I recommend adding to the struct. I named mine tdfvariables but you will want to name this something relevant to your project. Note the protocol defined here as well. This is a protocol that will be added to each viewController as an extension that defines that there should be a function to set its stateController member variable (which we have not defined yet, but will in a later step).
Step 4:
In my case I have 2 views controlled by the tabBarController. StandardRegimenViewController and settingsViewController. This is the code you will want to add for your viewControllers.
import UIKit
class SettingsViewController: UIViewController {
var stateController: StateController?
override func viewDidLoad() {
super.viewDidLoad()
}
}
//#JA - This adds the stateController variable to the viewController
extension SettingsViewController: StateControllerProtocol {
func setState(state: StateController) {
self.stateController = state
}
}
The extension here adds the protocol to your class and adds the function as required by it that we defined earlier in the stateController.swift file. This is what will eventually get the stateController and it's struct values into your viewController.
Step 5:
Use the stateController to get access to your variables! You are done!
Here is some examples of how I did this in one of my controllers.
stateController?.tdfvariables.lateBED = 100
You can read the variables the same way! The advantage of this approach is you are NOT using Singletons and instead Dependency Injection for your viewControllers and anything else that may need access to your variables. Read more about dependency injection to see the benefits vs singletons to learn more.
I have a tabbed view controller in my application and I use the same array for multiple tab views. I accomplish this by declaring the array outside of any classes (in the lines between import UIKit and the class declaration) so that it is essentially a global variable that every view can access. Have you tried this?
You can override the tabBar(didSelect:) method and then index the array of ViewControllers on the UITabViewController, and cast the ViewController to the desired Custom ViewController. No need for shared mutable state and all the problems that come with it.
class SecondViewController: UIViewController {
var array: [Int] = []
override func viewDidLoad() {
super.viewDidLoad()
}
}
class TabViewController: UITabBarController {
override func tabBar(
_ tabBar: UITabBar,
didSelect item: UITabBarItem
) {
super.tabBar(tabBar, didSelect: item)
var secondTab = viewControllers?[1] as? SecondViewController
secondTab?.array = [1, 2, 3]
}
}