Configuring a UIViewController outside scene(_:willConnectTo:options:) then setting it to window rootViewController doesn't work - appdelegate

My intention is to initialize a view controller and set it to the window rootViewController, then pass the view controller as dependency to an AppCoordinator where the app decide what to do with the view controller (replace it with an onboarding scene or login page, or even home screen if the user is already logged in.
The problem is that whenever I do that, the window doesn't seems to get back its view controller, but instead a black screen.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
var viewController = UIViewController()
self.window?.rootViewController = viewController
let appCoordinator = AppCoordinator(viewController: viewController)
appCoordinator.start()
window?.makeKeyAndVisible()
}
AppCoordinator.swift
import UIKit
import SwiftUI
class AppCoordinator: Coordinator {
var viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func start() {
let onboardingView = OnboardingView()
self.viewController = UIHostingController(rootView: onboardingView)
}
}
If I bypass the AppCoordinator and set everything in the scene delegate, the screen loads correctly:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
var viewController = UIViewController()
let onboardingView = OnboardingView()
self.window?.rootViewController = UIHostingController(rootView: onboardingView)
window?.makeKeyAndVisible()
}
What seems to be wrong when I pass the view controller as a dependency? Btw, I don't prefer to pass the window as dependency to the called coordinator, although this works, I don't prefer it.

Related

WKExtensionsDelegateClassName is Invalid in info.plist

So I am banging my head, I realized my stand along Watch App had a STUPID long name of "App Name - WatchKit App" so I went into my Target and changed the Display Name to "App Name" removing WatchKit App. Well now my app won't validate when uploading to the Appstore. I get the message - Invalid Info.plist key. The key WKExtensionDelegateClassName in bundle App Name.app/Watch/App Name WatchKit App.app is invalid.
My Info.plist has the value of
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
I have confirmed that I have #WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate in my #main for the SwiftUI App. And when I print a few values in my app launch I get the following confirmations:
Super Init - ExtensionDelegate
Contentview
applicationDidFinishLaunching for watchOS
Super Init - ExtensionDelegate
Optional(Wasted_Time_Watch_Extension.MeetingSetup)
Optional(Wasted_Time_Watch_Extension.MeetingStatistics)
Optional(Wasted_Time_Watch_Extension.Meeting)
applicationDidBecomeActive for watchOS
update complication
I create three classes at launch and print this in the log with print(ExtensionDelegate.shared.Setup as Any) , etc. The other lines are just confirming where I am at app startup.
This is a WatchOS8 application and I am running Xcode version Version 13.1 (13A1030d).
Update - Here's the entry in my plist
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
<key>WKWatchOnly</key>
And my App code
import SwiftUI
#if os(watchOS)
import ClockKit
#endif
struct DelegateKey: EnvironmentKey {
typealias Value = ExtensionDelegate
static let defaultValue: ExtensionDelegate = ExtensionDelegate()
}
extension EnvironmentValues {
var extensionDelegate: DelegateKey.Value {
get {
return self[DelegateKey.self]
}
set {
self[DelegateKey.self] = newValue
}
}
}
#main
struct WastedTimeWatchApp: App {
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate
let prefs: UserDefaults = UserDefaults(suiteName: suiteName)!
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environment(\.extensionDelegate, delegate)
}
}
}
}
class ExtensionDelegate: NSObject, WKExtensionDelegate, ObservableObject {
#Environment(\.extensionDelegate) static var shared
// variables removed to simplify posting
override init() {
print("Super Init - ExtensionDelegate")
super.init()
}
func applicationDidFinishLaunching() {
print("applicationDidFinishLaunching for watchOS")
ExtensionDelegate.shared.meetingSetup = MeetingSetup()
print(ExtensionDelegate.shared.meetingSetup as Any)
ExtensionDelegate.shared.meetingStatistics = MeetingStatistics()
print(ExtensionDelegate.shared.meetingStatistics as Any)
ExtensionDelegate.shared.meeting = Meeting()
print(ExtensionDelegate.shared.meeting as Any)
}
func applicationDidBecomeActive() {
print("applicationDidBecomeActive for watchOS")
print("update complication")
let server = CLKComplicationServer.sharedInstance()
for complication in server.activeComplications ?? [] {
server.reloadTimeline(for: complication)
}
}
func applicationDidBecomeInactive() {
print("update complication")
let server = CLKComplicationServer.sharedInstance()
for complication in server.activeComplications ?? [] {
server.reloadTimeline(for: complication)
}
print("applicationDidBecomeInactive for watchOS")
}
}
I figured this out... I had duplicated the plist entry in both the WatchKit App and WatchKit Extension plist file. Removed it from the list WatchKit Extension plist and all is working fine.

Why Xcode says that 'instance will be immediately deallocated'?

I'm trying to find out why am I getting the following message in Xcode:
Instance will be immediately deallocated because property 'delegate' is 'weak'
Here's a code:
import ARKit
class OtherViewController: NSObject, ARSCNViewDelegate { }
class ViewController: UIViewController, ARSCNViewDelegate {
var arView = ARSCNView(frame: .zero)
let otherVC = OtherViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'M USING ONE OF THESE
arView.delegate = self // works fine
arView.delegate = otherVC // works fine
arView.delegate = ViewController() // PRINTS WARNING
}
}
Question:
I understand that the delegate is a weak and optional:
weak var delegate: ARSCNViewDelegate? { get set }
However, why arView.delegate = self or arView.delegate = otherVC works FINE but arView.delegate = ViewController() prints a warning?
By setting arView.delegate = ViewController() you’re creating a new instance of ViewController that isn’t retained anywhere so it’s deallocated as soon as the function returns.

Back button not being called in TabbarCoordinator in horizontal flow iOS 12

Coordinator pattern is an old topic with many libraries trying to solve it and I am learning it in simple example app.
My current set up is 3 rootViewControlers: LoadingStateCoordinator, WelcomeCoordinator, TabBarCoordinator but missing connection between UIKit and coordinators. I am trying to implement it with a UINavigationController but the button is not being called. I need a way to connect to back button and a reusable coordinator that I could push to and dellocate accordingly (that is without RxSwift).*Set up Welcome screen as the parent/main navigation and always be able to come back to it.**
So after user selects a form from modal view (vertical flow) presented I show on a push a TabBarCoordinator (horizontal). All viewControllers have empty.storyboard, UIViewController and Coordinator exept the TabBar.Here I only have a coordinator due to the set up of child tab coordinators and the magic needs to happen on a back button tap. Currenly this only being called when user comes from LoadingStateCoordinator. There I need to send the user back to the Welcome screen so they can change the onboarding set up. Here is the first code for LoadingStateCoordinator:
final class LoadingStateCoordinator: NSObject, Coordinator {
*// MARK: - Inputs required*
var childCoordinators: [Coordinator]
var presenter: UINavigationController
private let window: UIWindow
*// MARK: - Initialization*
init(window: UIWindow) {
self.window = window
childCoordinators = []
presenter = UINavigationController()
}
*// MARK: - Coordinator*
func start() {
let controller: LoadingStateViewController = LoadingStateViewController.instantiate()
window.rootViewController = controller
controller.delegate = self
}
}
*// MARK: - LoadingViewControllerDelegate*
extension LoadingStateCoordinator : LoadingViewControllerDelegate {
func performScreenSwitch() {
if UserDefaults.standard.userWasHere == false {
let tabCoordinator: TabBarCoordinator = TabBarCoordinator(window: window, tabBarController: UITabBarController())
window.rootViewController = presenter
addChildCoordinator(tabCoordinator)
tabCoordinator.start()
presenter.pushViewController(tabCoordinator.tabBarController!, animated: true)
} else {
let welcomeCoordinator = WelcomeCoordinator(window: window, presenter: presenter)
window.rootViewController = welcomeCoordinator.presenter
addChildCoordinator(welcomeCoordinator)
welcomeCoordinator.start()
}
}
}
And here is the TabBarCoordinator that need to perform back to Welcome screen action. When I present popToRootfunction it pushes the Welcome screen but all the button there are disbled. I guess to be retain cycle issue. Do I need funadametally another set up? Is there a way to popToRoot(vc) in this set up? What I tryed ended with runtime error "poping to non existing controller".
TabBarCoordinator code that need to perform this:
final class TabBarCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
internal var tabBarController: UITabBarController?
internal var childCoordinators: [Coordinator]
var parentCoordinator: LoadingStateCoordinator?
lazy var leftBtn: UIBarButtonItem = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.turn.up.left"), for: .normal)
button.sizeToFit()
button.addTarget(self,
action: #selector(self.popToRoot(_:)),
for: .touchUpInside)
return UIBarButtonItem(customView: button)
}()
init(window: UIWindow, tabBarController: UITabBarController) {
self.tabBarController = tabBarController
childCoordinators = []
self.presenter = UINavigationController()
}
func start() {
performGetTabBar()
self.presenter.delegate = self
}
private func performGetTabBar() {
let coordinators: [Coordinator] = generateTabCoordinators()
coordinators.forEach({ coordinator in
coordinator.start()
addChildCoordinator(coordinator)
})
let presenters: [UIViewController] = coordinators.map({ coordinator -> UIViewController in
return coordinator.presenter
})
leftBtn.style = .plain
tabBarController?.navigationItem.leftBarButtonItem = leftBtn
tabBarController?.setViewControllers(presenters, animated: false)
selectTab(type: SurfTripCoordinator.self)
}
private func generateTabCoordinators() -> [Coordinator] {
let calculatorCoordinator: CalculatorCoordinator = CalculatorCoordinator(presenter: UINavigationController())
let tripCoordinator: SurfTripCoordinator = SurfTripCoordinator(presenter: UINavigationController())
let sellCoordinator: SavedTripsCoordinator = SavedTripsCoordinator(presenter: UINavigationController())
return [calculatorCoordinator, tripCoordinator, sellCoordinator]
}
*//this is not being called when coming from vertical flow*
#objc func popToRoot(_ sender: UIBarButtonItem) {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
tabBarController?.navigationController?.pushViewController(controller, animated: true)
}
}
extension TabBarCoordinator: UINavigationControllerDelegate {
func selectTab<T: Coordinator>(type _: T.Type) {
guard let index = childCoordinators.firstIndex(where: { coordinator in
coordinator is T
}) else {
return
}
tabBarController?.selectedIndex = index
}
}
and here is the current WelcomeCoordinator set up
class WelcomeCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
var childCoordinators: [Coordinator]
init(window: UIWindow, presenter: UINavigationController) {
self.presenter = presenter
childCoordinators = []
}
func start() {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
controller.delegate = self
presenter.pushViewController(controller, animated: true)
}
}
extension WelcomeCoordinator : WelcomeViewControllerDelegate {
func performAddLevel() {
let addLevelCoordinator: AddLevelViewCoordinator = AddLevelViewCoordinator(presenter: UINavigationController())
addLevelCoordinator.start()
addChildCoordinator(addLevelCoordinator)
addLevelCoordinator.presenter.modalPresentationStyle = .fullScreen
presenter.present(addLevelCoordinator.presenter, animated: true, completion: nil)
}
}
sorry for the long post I wish there was more reaktive native way to do this...
Ok so I found partlly a solution the back button solution for my case: not using pushViewController or show because it comes with back button. presenter.setViewControllers([tabCoordinator.tabBarController!], animated: true) and there setting the navBar to hidden. I made my own navItem button to navigate to rootVC. Next step to allocate and remove all child tabBar coordinators on back tap recognized.

CFNetwork SSLHandshake failed (-9807)

I keep receiving this error, CFNetwork SSLHandshake failed (-9807), in the debug window and have no data displayed when trying to populate a UITableViewController with Firebase data. I have tried this potential solution iOS 9 ATS and Firebase REST but still have the issue.
The code I am using is (Credit to #DavidEast)
class TableViewController1: UITableViewController {
// your firebase reference as a property
var ref: Firebase!
// your data source, you can replace this with your own model if you wish
var items = [FDataSnapshot]()
override func viewDidLoad() {
super.viewDidLoad()
// initialize the ref in viewDidLoad
ref = Firebase(url:"https://the-lighthouse-app.firebase.io/states")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// listen for update with the .Value event
ref.observeEventType(.Value) { (snapshot: FDataSnapshot!) in
var newItems = [FDataSnapshot]()
// loop through the children and append them to the new array
for item in snapshot.children {
newItems.append(item as! FDataSnapshot)
}
// replace the old array
self.items = newItems
// reload the UITableView
self.tableView.reloadData()
}
}
}

Having a UINavigationController in the master view of a UISplitViewController in iOS 8

In my UISplitViewController, the master view is a UINavigationController containing a UITableViewController. Sometime, when the user selects an item in the table, I have to push another UITableViewController over the existing table in the master view.
In iOS 7, inside my first UITableViewController I just call
[self.navigationController pushViewController:otherTableVC animated:YES];
In iOS 8:
When the split view is collapsed, the otherTableVC becomes the detail View!
Then after rotating the device, we see the two tables side by side...
Worse, if the device shows the two panes, the code works great and the second table is pushed over the first one in the master view. But, after a double rotation, the two tables are again side by side. It seems the collapsed mode of the UISplitViewController interferes with my own navigation controller…
How can I manage my own UINavigationController in the Master View?
Thank you
EDITED:
My both primary and details views have a navigation Controller. And to solve my problem, I just discovered that, in collapsed mode, I have to create an extra Navigation Controller and push it over the primary navigation controller.
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:otherTableVC];
[self.navigationController pushViewController:navController animated:YES];
So I just discovered hat we can push a navigation Controller inside another Navigation Controller.
Short answer, you can control this behaviour via the UISplitViewControllerDelegate methods:
splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:
splitViewController:separateSecondaryViewControllerFromPrimaryViewController:
I suspect what you really want to do is deal with the situation where you have an iOS 8 UISplitViewController-based app where your primary and detailed views are both UINavigationControllers and there are some viewControllers (within these navigation controllers) that you want to appear only on the primary or detail side of the split view. The answer below deals with this. It also copes with the situation where you sometimes wish for a view to replace the views in the Detail navigation controller, rather than getting pushed there.
A small caveat: the code below does not deal with all possible cases and has some assumptions:
We don't expect anything can change on the Detailed navigation controller stack when the split view is collapsed and those views are obscured by a detailed view above them.
Our UIViewController subclasses all have a shouldDisplayInDetailedView and shouldReplaceDetailedView property
We assume that we only push views onto the detailed navigation controller that have the shouldDisplayInDetailedView property set.
View controllers are added to the Detail side via splitViewController:showDetailViewController: or a pushViewController:animated: on the navigationController property of a view within a detailed view (in either expanded or collapsed state).
View controllers that should replace the view controllers in the Detail navigation controller are only added via splitViewController:showDetailViewController: and only from interaction with view in the Primary view controller, i.e., this can only happen if the Primary view controller is not obscured when in collapsed state.
We have a BlankViewController to display in the Detail View when the split view controller gets expanded but we only have view controllers that should stay on the Primary side.
I don't recommend implementing only one side of the splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: / splitViewController: separateSecondaryViewControllerFromPrimaryViewController: logic and depending on the default implementation for the other side. Apple do some strange things like putting the UINavigationViewController from the Detail side into the primary side as one of the viewControllers in the Primary navigation controller stack but then pushing other view controllers above it, which even if you understand completely still can't be replicated from your own code. Thus its best to handle both sides of the process yourself.
This is what I use:
#pragma mark -
#pragma mark Split View Controller delegate.
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showViewController:(UIViewController *)vc sender:(id)sender
{
//Standard behaviour. This won't get called in our case when the split view is collapsed and the primary view controllers are obscured.
return NO;
}
// Since we treat warnings as errors, silence warning about unknown selector below on UIViewController subclasses.
#pragma GCC diagnostic ignored "-Wundeclared-selector"
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)vc sender:(id)sender
{
if (splitViewController.collapsed == NO)
{
// The navigation controller we'll be adding the view controller vc to.
UINavigationController *navController = splitViewController.viewControllers[1];
UIViewController *topDetailViewController = [navController.viewControllers lastObject];
if ([topDetailViewController isKindOfClass:[BlankViewController class]] ||
([vc respondsToSelector:#selector(shouldReplaceDetailedView)] && [vc performSelector:#selector(shouldReplaceDetailedView)]))
{
// Replace the (expanded) detail view with this new view controller.
[navController setViewControllers:#[vc] animated:NO];
}
else
{
// Otherwise, just push.
[navController pushViewController:vc animated:YES];
}
}
else
{
// Collapsed. Just push onto the conbined primary and detailed navigation controller.
UINavigationController *navController = splitViewController.viewControllers[0];
[navController pushViewController:vc animated:YES];
}
// We've handled this ourselves.
return YES;
}
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController
{
UINavigationController *primaryNavController = (UINavigationController *)primaryViewController;
UINavigationController *secondaryNavController = (UINavigationController *)secondaryViewController;
UIViewController *bottomSecondaryView = [secondaryNavController.viewControllers firstObject];
if ([bottomSecondaryView isKindOfClass:[BlankViewController class]])
{
NSAssert([secondaryNavController.viewControllers count] == 1, #"BlankViewController is not only detail view controller");
// If our secondary controller is blank, do the collapse ourself by doing nothing.
return YES;
}
// We need to shift these view controllers ourselves.
// This should be the primary views and then the detailed views on top.
// Otherwise the UISplitViewController does wacky things like embedding a UINavigationController inside another UINavigation Controller, which causes problems for us later.
NSMutableArray *newPrimaryViewControllers = [NSMutableArray arrayWithArray:primaryNavController.viewControllers];
[newPrimaryViewControllers addObjectsFromArray:secondaryNavController.viewControllers];
primaryNavController.viewControllers = newPrimaryViewControllers;
return YES;
}
- (UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
{
UINavigationController *primaryNavController = (UINavigationController *)primaryViewController;
// Split up the combined primary and detail navigation controller in their component primary and detail view controller lists, but with same ordering.
NSMutableArray *newPrimaryViewControllers = [NSMutableArray array];
NSMutableArray *newDetailViewControllers = [NSMutableArray array];
for (UIViewController *controller in primaryNavController.viewControllers)
{
if ([controller respondsToSelector:#selector(shouldDisplayInDetailedView)] && [controller performSelector:#selector(shouldDisplayInDetailedView)])
{
[newDetailViewControllers addObject:controller];
}
else
{
[newPrimaryViewControllers addObject:controller];
}
}
if (newDetailViewControllers.count == 0)
{
// If there's no detailed views on the top of the navigation stack, return a blank view (in navigation controller) for detailed side.
UINavigationController *blankDetailNavController = [[UINavigationController alloc] initWithRootViewController:[[BlankViewController alloc] init]];
return blankDetailNavController;
}
// Set the new primary views.
primaryNavController.viewControllers = newPrimaryViewControllers;
// Return the new detail navigation controller and views.
UINavigationController *detailNavController = [[UINavigationController alloc] init];
detailNavController.viewControllers = newDetailViewControllers;
return detailNavController;
}
Swift 4 Version with minor changes to make it work with my code:
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if !isCollapsed {
// in expanded mode set new VC as top view controller of the detail nav controller
if let detailNavigationController = viewControllers[1] as? UINavigationController {
detailNavigationController.setViewControllers([vc], animated: false)
}
} else {
// in collapsed mode push the new view controller on the master nav controller
if let masterNavigationController = viewControllers[0] as? UINavigationController {
masterNavigationController.pushViewController(vc, animated: true)
}
}
return true
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
let masterNavigationController = primaryViewController as? UINavigationController
let detailNavigationController = secondaryViewController as? UINavigationController
let episodeDetailViewController = detailNavigationController?.viewControllers.first as? EpisodeDetailTableViewController
if episodeDetailViewController?.episode == nil {
// detail view is blank. We do not need to push this onto the master
return true
}
guard var newMasterViewControllers = masterNavigationController?.viewControllers else { return false }
newMasterViewControllers.append(contentsOf: detailNavigationController?.viewControllers ?? [])
masterNavigationController?.setViewControllers(newMasterViewControllers, animated: false)
return true
}
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
let masterNavigationViewController = primaryViewController as? UINavigationController
var newMasterViewControllers = [UIViewController]()
var newDetailViewControllers = [UIViewController]()
for vc in masterNavigationViewController?.viewControllers ?? [] {
if vc is PodcastsTableViewController || vc is EpisodesTableViewController {
newMasterViewControllers.append(vc)
} else {
newDetailViewControllers.append(vc)
}
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let detailNavigationController = storyboard.instantiateViewController(withIdentifier: "splitViewDetailViewController") as! UINavigationController
if newDetailViewControllers.count == 0 {
let emptyEpisodeDetailViewController = storyboard.instantiateViewController(withIdentifier: "episodeDetail")
newDetailViewControllers.append(emptyEpisodeDetailViewController)
}
masterNavigationViewController?.setViewControllers(newMasterViewControllers, animated: false)
detailNavigationController.setViewControllers(newDetailViewControllers, animated: false)
return detailNavigationController
}

Resources