Programmatically Setting Root View Controller in Swift - ios

I'm not sure why this is so trivial, but my Swift translation has been a slow one. Anyways, I can't figure out what it is complaining about. All I am trying to do is set the root view controller, and the compiler spits an error saying:
"Splash pageController does not have a member named init"
Here's my app delegate:
var window: UIWindow?
var CLIENT_KEY = c_key()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.mainScreen().bounds)
var url_string = "XXXX=\(CLIENT_KEY.client_key)"
var g_home_url = String.stringWithContentsOfURL(NSURL.URLWithString(url_string), encoding: NSUTF8StringEncoding, error: nil)
if (g_home_url? != nil){
NSUserDefaults.standardUserDefaults().setObject(g_home_url, forKey: "showUrl")
}
let DocumentsDirectory = NSSearchPathDirectory.DocumentationDirectory
let UserDomainMask = NSSearchPathDomainMask.UserDomainMask
if let paths = NSSearchPathForDirectoriesInDomains(DocumentsDirectory, UserDomainMask, true){
if paths.count > 0{
if let dirPath = paths[0] as? String {
// let url: NSURL = NSURL.URLWithString("\(g_home_url)/XXX\(CLIENT_KEY.client_key)")
// println(url)
var err: NSError?
var g_home_url = NSUserDefaults.standardUserDefaults().objectForKey("showUrl") as String
println(g_home_url)
var image = UIImage(data: NSData(contentsOfURL: NSURL.URLWithString("\(g_home_url)XXX=\(CLIENT_KEY.client_key)")))
let writePath = dirPath.stringByAppendingPathComponent("splash_page.png")
UIImagePNGRepresentation(image)
}
}
}
//This is where the error shows up
var rootview: SplashViewController = SplashViewController()
if let window = window {
window.rootViewController = rootview;
}
SplashViewController
class SplashViewController: UIViewController {
}
Very basic I know. Normally, in Objective-C I would init a nib name and set the view controller to that. What's the main difference in Swift?

This line:
var rootview: SplashViewController = SplashViewController()
...calls SplashViewController's init method. But you have not given SplashViewController any init method. Give it one.
What I usually do is specify the nib in my init override; for example:
override init() {
super.init(nibName:"SplashViewController", bundle:nil)
}
If you don't do something along those lines, the view controller won't find its nib and you'll end up with a black screen.
Note that that code will very likely net you a new error message complaining that you didn't implement init(coder:); so you'll have to implement it, even if only to throw an error if it is accidentally called:
required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}

Related

Test different behaviours in AppDelegate for each or separated unit and integration tests

I want to test my application's behavior that decided on app launch. For example: In a tab bar controller, how many and which tabs will be created is been decided on app launch where the root window has been created so I want to test these behaviors for each test case.
This new feature is set via A/B service and the value retrieved only during app launching. Based on that value, the tab bar's view controllers are set.
For example:
var viewControllers: [UIViewController] = [ tabOne, tabTwo]
if Config.isNewFeatureEnabled {
viewControllers.append(self._menuCoordinator.rootViewController)
} else {
viewControllers.append(self._anotherTabBarController)
viewControllers.append(self._anotherCoordinator.rootViewController)
viewControllers.append(self._someOtherCoordinator.rootViewController)
}
_tabBarController.viewControllers = viewControllers
Let me put in code, in order to make tests easy I created a protocol (not necessarily but better approach for injection)
protocol FeatureFlag {
var isNewFeatureEnabled: Bool { get set }
}
// Implementation
class FeatureFlagService: FeatureFlag {
var isNewFeatureEnabled = false
// Bunch of other feature flags
}
In my test cases I want to switch the config with out effecting other side of the app. Something like this:
class NewFeatureVisibilityTests: XCTestCase {
func test_TabBar_has_threeTabs_when_NewFeature_isEnabled() {
// Looking for a way to inject the config
let tabBar = getKeyWindow()?.rootViewController as? UITabBarController
guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
return XCTFail("Expected root view controller to be a tab bar controller")
}
XCTAssertEqual(tabBar.viewControllers?.count, 3)
}
func test_TabBar_has_fiveTabs_when_NewFeature_isDisabled() {
// Looking for a way to inject the config
let tabBar = getKeyWindow()?.rootViewController as? UITabBarController
guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
return XCTFail("Expected root view controller to be a tab bar controller")
}
XCTAssertEqual(tabBar.viewControllers?.count, 5)
}
}
What I want is set application's behaviour through injection (a config etc) for each test case.
One test the feature will be enabled, other test will assert the feature disabled state.
Create a config property in AppDelegate using existing type of FeatureFlag along with a default value on override init.
extension UIApplication {
var currentWindow: UIWindow {
return (connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?.windows
.filter({$0.isKeyWindow}).first!)!
}
}
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let config: FeatureFlag!
override init() {
config = FeatureFlagService()
}
init(config: FeatureFlag!) {
self.config = config
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Create a tabBar with 3 tabs
let tabBarController = UITabBarController()
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
let fourthViewController = UIViewController()
let fifthViewController = UIViewController()
firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 1)
thirdViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .more, tag: 2)
fourthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 3)
fifthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 4)
var viewControllers = [firstViewController, secondViewController]
if config.isNewFeatureEnabled {
viewControllers.append(thirdViewController)
} else {
viewControllers.append(fourthViewController)
viewControllers.append(fifthViewController)
}
tabBarController.viewControllers = viewControllers
// Create a window and set the root view controller
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = tabBarController
window.makeKeyAndVisible()
self.window = window
return true
}
}
And in tests, I set my config, create an instance of AppDelegate, inject the config, and launching the application through appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) function of AppDelegate.
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
Tests:
import XCTest
#testable import ExampleApp
final class NewFeatureVisibilityTests: XCTestCase {
func test_app_can_start_with_isNewFeatureEnabled(){
let config = FeatureFlagService()
config.isNewFeatureEnabled = true
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
return XCTFail("RootViewController is nil")
}
XCTAssertEqual(rootVC.viewControllers?.count, 3)
}
func test_app_can_start_with_isNewFeatureDisabled(){
let config = FeatureFlagService()
config.isNewFeatureEnabled = false
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
return XCTFail("RootViewController is nil")
}
XCTAssertEqual(rootVC.viewControllers?.count, 4)
}
}

Swift. After addSubview click listeners dont work

I have a testVC. TestVC hasnt storyboard, this viewController has XIB file. I show this VC when i have no internet. And logic for a show this VC like this:
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
also i have extension for UIViewController
extension UIViewController {
var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var sceneDelegate: SceneDelegate? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate else { return nil }
return delegate
}
}
extension UIViewController {
var window: UIWindow? {
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
return window
}
guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { return nil }
return window
}
}
all clicks in the TestVC works if i show this View Controller like this:
navController.pushViewController(NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil), animated: true)
but it doesn't suit me. I need to show NoInternetConnectionVC like i described above, that is, like this. When i show NoInternetConnectionVC like below all my listeners stop to work
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
I tried to add line isUserInteractionEnabled to my code, like this
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.isUserInteractionEnabled = true //added line
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
but it doesnt work
You have just added it as a subview. After adding subview move your controller to parent.
if let root = UIApplication.shared.windows.first?.rootViewController {
root.addChild(getVC)
getVC.didMove(toParent: root)
}

How to add an Avatar image in GetStream iOS Activity Feed component?

My config: XCode 10.3, Swift 5, MacOS Catalina v10.15
I followed the native iOS Activity Feed demo (https://getstream.io/ios-activity-feed/tutorial/?language=python) to successfully add an activity feed to my XCode project.
How do I add an avatar image for each user? Here is what I have tried so far:
I uploaded an avatar image to my backend storage, obtained the corresponding URL, and used a json object to create a new user using my backend server like so:
{
"id" : "cqtGMiITVSOLE589PJaRt",
"data" : {
"name" : "User4",
"avatarURL" : "https:\/\/firebasestorage.googleapis.com\/v0\/b\/champXXXXX.appspot.com\/o\/profileImage%2FcqtGMiITVSOLXXXXXXXX"
}
}
Verified that user was created successfully, but the FlatFeedPresenter view controller shows up with a blank avatar image even though activities in the feed show up correctly. How can I use the user's data.avatarURL property to populate the avatar image correctly?
Here is the StreamActivity ViewController class behind the Main storyboard.
import UIKit
import GetStream
import GetStreamActivityFeed
class StreamActivityViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {
let textToolBar = TextToolBar.make()
override func viewDidLoad() {
if let feedId = FeedId(feedSlug: "timeline") {
let timelineFlatFeed = Client.shared.flatFeed(feedId)
presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: [.likes, .comments])
}
super.viewDidLoad()
setupTextToolBar()
subscribeForUpdates()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailViewController = DetailViewController<GetStreamActivityFeed.Activity>()
detailViewController.activityPresenter = activityPresenter(in: indexPath.section)
detailViewController.sections = [.activity, .comments]
present(UINavigationController(rootViewController: detailViewController), animated: true)
}
func setupTextToolBar() {
textToolBar.addToSuperview(view, placeholderText: "Share something...")
// Enable image picker
textToolBar.enableImagePicking(with: self)
// Enable URL unfurling
textToolBar.linksDetectorEnabled = true
textToolBar.sendButton.addTarget(self,
action: #selector(save(_:)),
for: .touchUpInside)
textToolBar.updatePlaceholder()
}
#objc func save(_ sender: UIButton) {
// Hide the keyboard.
view.endEditing(true)
if textToolBar.isValidContent, let presenter = presenter {
// print("Message validated!")
textToolBar.addActivity(to: presenter.flatFeed) { result in
// print("From textToolBar: \(result)")
}
}
}
}
UPDATE:
I updated the AppDelegate as suggested in the answer below, but avatar image still does not update even though rest of the feed does load properly. Set a breakpoint at the following line and found that avatarURL property of createdUser is nil even though streamUser.avatarURL is set correctly.
print("createdUser: \(createdUser)")
Updated AppDelegate code (had to comment out
initialViewController?.reloadData() to address a "Value of type 'UIViewController' has no member 'reloadData'" error -- not sure whether is contributing to the avatar issue.)
import UIKit
import Firebase
import GetStream
import GetStreamActivityFeed
import GoogleSignIn
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
GIDSignIn.sharedInstance()?.clientID = FirebaseApp.app()?.options.clientID
Database.database().isPersistenceEnabled = true
configureInitialRootViewController(for: window)
return true
}
}
extension AppDelegate {
func configureInitialRootViewController(for window: UIWindow?) {
let defaults = UserDefaults.standard
let initialViewController: UIViewController
if let _ = Auth.auth().currentUser, let userData = defaults.object(forKey: Constants.UserDefaults.currentUser) as? Data, let user = try? JSONDecoder().decode(AppUser.self, from: userData) {
initialViewController = UIStoryboard.initialViewController(for: .main)
AppUser.setCurrent(user)
Client.config = .init(apiKey: Constants.Stream.apiKey, appId: Constants.Stream.appId, token: AppUser.current.userToken)
let streamUser = GetStreamActivityFeed.User(name: user.name, id: user.id)
let avatarURL = URL(string: user.profileImageURL)
streamUser.avatarURL = avatarURL
Client.shared.create(user: streamUser) { [weak initialViewController] result in
if let createdUser = try? result.get() {
print("createdUser: \(createdUser)")
// Refresh from here your view controller.
// Reload data in your timeline feed:
// initialViewController?.reloadData()
}
}
} else {
initialViewController = UIStoryboard.initialViewController(for: .login)
}
window?.rootViewController = initialViewController
window?.makeKeyAndVisible()
}
}
The recommended approach is to ensure the user exists on Stream's side in AppDelegate.
extension AppDelegate {
func configureInitialRootViewController(for window: UIWindow?) {
let defaults = UserDefaults.standard
let initialViewController: UIViewController
if let _ = Auth.auth().currentUser, let userData = defaults.object(forKey: Constants.UserDefaults.currentUser) as? Data, let user = try? JSONDecoder().decode(AppUser.self, from: userData) {
initialViewController = UIStoryboard.initialViewController(for: .main)
AppUser.setCurrent(user)
Client.config = .init(apiKey: Constants.Stream.apiKey,
appId: Constants.Stream.appId,
token: AppUser.current.userToken,
logsEnabled: true)
let streamUser = GetStreamActivityFeed.User(name: user.name, id: user.id)
streamUser.avatarURL = user.avatarURL
// ensures that the user exists on Stream (if not it will create it)
Client.shared.create(user: streamUser) { [weak initialViewController] result in
if let createdUser = try? result.get() {
Client.shared.currentUser = createdUser
// Refresh from here your view controller.
// Reload data in your timeline feed:
// flatFeedViewController?.reloadData()
}
}
} else {
initialViewController = UIStoryboard.initialViewController(for: .login)
}
window?.rootViewController = initialViewController
window?.makeKeyAndVisible()
}
}

Swift - CoreData Passing PersistentController to many ViewControllers through UITabBarController and UINavigationControllers

I'm developing my first app and was injecting ManagedObjectContext from AppDelegate.
Yesterday I found a new approach and created separated CoreDataStack like here.
And my app storyboard is like that:
My storyboard
There are one UITabBarController, two UINavigationControllers. And you can see which view controller's use CoreData.
Questions:
Pass my CoreDataStack to every view controller, that uses CoreData. Is my approach is correct or not?
Appdelegate's code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
coreDataStack = CoreDataStack(){
(response) -> Void in
self.completeAnySetup()
}
//Pass NSManagedObjectContext to many VCs in TabBarVC
let mainTabBarVC: UITabBarController = self.window!.rootViewController as! UITabBarController
if let navControllerOne = mainTabBarVC.viewControllers![0] as? UINavigationController {
let mainMenuVC = navControllerOne.viewControllers[0] as! MainMenuVC
mainMenuVC.coreDataStack = self.coreDataStack
}
return true }
Then from navControllerOne.viewControllers[0] i inject it into next VC:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == Segues.TrackedHistoryVC {
if let trackedHistoryVC = segue.destinationViewController as? TrackedHistoryVC {
trackedHistoryVC.hidesBottomBarWhenPushed = true
let backItem = UIBarButtonItem()
backItem.title = ""
navigationItem.backBarButtonItem = backItem
if let coreDataStack = coreDataStack {
trackedHistoryVC.coreDataStack = coreDataStack
}
}
}}
And so on. But i think it's more complicated that just old way: let appDelegate = ..., let psc = .... .
Now after this manipulations after fetching data from CoreData it's give me just a empty object and also save an empty object. And i don't know why?
For example my function to fetch objects:
func fetchAndResult() {
if let mainContext = mainContext {
let fetchRequest = NSFetchRequest(entityName: "TrackedShipments")
do {
let results = try self.mainContext!.executeFetchRequest(fetchRequest)
print(results)
self.trackedShipments = results as! [TrackedShipments]
} catch { print("Error") }
}
}
A lot of people say that use CoreDataStack Singleton is not good because it's not safe or other reasons. Then is there any approach to make this Singleton be safer or may be it possible to create a singleton with multiple managedObjectContexts?
UPDATE
I looked a lot to find a right way and stopped on that:
private(set) var persistenceController: PersistenceController? = nil
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
persistenceController = PersistenceController(completion: { (result, failError) -> Void in
print("Persistent controller is loaded")
})
guard let mainTabBarVC: UITabBarController = self.window!.rootViewController as? UITabBarController else {fatalError()}
guard let navControllerOne = mainTabBarVC.viewControllers![0] as? UINavigationController else { fatalError() }
guard let navControllerTwo = mainTabBarVC.viewControllers![1] as? UINavigationController else { fatalError() }
guard let mainMenuVC = navControllerOne.topViewController as? MainMenuVC else { fatalError() }
guard let myCabinetVC = navControllerTwo.topViewController as? MyCabinetVC else { fatalError() }
mainMenuVC.persistenceController = persistenceController
myCabinetVC.persistenceController = persistenceController
return true
}
It works very well. So only appdelegate can set persistence controller(Core Data stack), and other view controllers can only read it.
I found answer for that. It was very stupid mistake. After creating new Core Data stack i did't give addPersistentStoreWithType a right store URL.

Why is the data not being saved?

I am getting a fatal error: unexpectedly found nil while unwrapping an Optional value when I am trying to open a notification onto a certain view controller. I think I know why, there are some variables which are nil, causing the app to crash, but I am trying to give those variables data, but they are not saving.
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
PFPush.handlePush(userInfo)
let text = userInfo["aps"]!["alert"]
let title = userInfo["aps"]!["alert"]!!["title"]
let artist = userInfo["aps"]!["alert"]!!["artist"]
print(text)
print(title)
print(artist)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("PlayerController") as! PlayerViewController
vc.dataModel.artistplay = artist as? String
vc.dataModel.titleplay = title as? String
window?.rootViewController = vc
}
This is the code for the PlayerViewController (The view controller I am trying to open when the push notification is opened)
#IBOutlet weak var playertitle: UILabel!
#IBOutlet weak var playerartist: UILabel!
var dicData : NSDictionary?
var dataModel : DataModelTwo?
var data: NSDictionary?
var shareDataModel: Share?
var buttonState: Int = 0;
override func viewWillAppear(animated: Bool) {
playertitle.text = dataModel!.titleplay
playerartist.text = dataModel!.artistplay
}
do NOT set UILable text from AppDelegate.
set a variable in your ViewController then use it in viewDidLoad.
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
PFPush.handlePush(userInfo)
let text = userInfo["aps"]!["alert"]
print(text)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("PlayerController") as! PlayerViewController
vc.foo = "bar"
window?.rootViewController = vc
}
use variables and update UI :
override func viewDidLoad() {
print(self.foo) // output is "bar"
}
From the code you show, the dataModel in PlayerViewController is not initialized and, as it is an optional, its value is nil. Since you're implicitly unwrapping - dataModel! it causes the crash.
Initialize first the dataModeland then use it to get the values you want.

Resources