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

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()
}
}

Related

Thread Error in Xcode w/ Swift When Launching New Screen

Once the user logs into my app, I set the window, set the rootViewController and makeKeyAndVisible and when I do I get a Thread 9: signal SIGABRT error. FWIW, I get a purple warning -[UIWindow initWithFrame:] must be used from main thread only when setting self.window = UIWindow(frame: UIScreen.main.bounds). See code below to see my setup.
The code dies with that error right here - self.window!.makeKeyAndVisible() in the launchWindow(aWindow: UIWindow?) function below in AppController.swift.
AppDelegate.swift
//
// AppDelegate.swift
import UIKit
import AWSCognitoAuth
import AWSSNS
import AWSCognitoIdentityProvider
import UserNotifications
import ESTabBarController_swift
import AWSMobileClient
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var navigationController: UINavigationController?
var storyboard: UIStoryboard?
var loginViewController: LoginViewController?
var pool = AWSCognitoIdentityUserPool.init(forKey: "UserPool")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppController.sharedInstance.enableCognitoClientWithAuthentication()
// setup logging
// pool.delegate = self
// AWSDDLog.sharedInstance.logLevel = .verbose
// let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .USEast1,
// identityPoolId: Constants.APIKeys.AWSIdentityPoolID)
//
// // setup service configuration
// let serviceConfiguration = AWSServiceConfiguration(region: AWSRegionType.USEast1, credentialsProvider: credentialsProvider)
//
// // create pool configuration
// let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: Constants.APIKeys.AWSClientID,
// clientSecret: Constants.APIKeys.AWSSecret,
// poolId: Constants.APIKeys.AWSPoolID)
// AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration
// // initialize user pool client
// AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: poolConfiguration, forKey: "UserPool")
//
// pool.currentUser()?.getSession()
// fetch the user pool client we initialized in above step
//let pool = AWSCognitoIdentityUserPool(forKey: "UserPool")
let signedIn = AWSMobileClient.sharedInstance().isSignedIn
self.navigationController = UINavigationController()
if !signedIn {
navigationInit()
}
// } else {
// AppController.sharedInstance.showLoggedInStateAndReturn(true)
// }
//pool.delegate = self
self.window = UIWindow(frame: UIScreen.main.bounds)
AppController.sharedInstance.launchInWindow(aWindow: self.window)
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
navigationInit()
return true
}
func navigationInit() {
let loginViewController = LoginViewController()
self.navigationController!.pushViewController(loginViewController, animated: false)
}
//MARK: Push Notification
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
/// Attach the device token to the user defaults
var token = ""
for i in 0..<deviceToken.count {
token = token + String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
print(token)
UserDefaults.standard.set(token, forKey: "deviceTokenForSNS")
/// Create a platform endpoint. In this case, the endpoint is a
/// device endpoint ARN
let sns = AWSSNS.default()
let request = AWSSNSCreatePlatformEndpointInput()
request?.token = token
request?.platformApplicationArn = Constants.APIKeys.AWSSSNSARN
sns.createPlatformEndpoint(request!).continueWith(executor: AWSExecutor.mainThread(), block: { (task: AWSTask!) -> AnyObject? in
if task.error != nil {
print("Error: \(String(describing: task.error))")
} else {
let createEndpointResponse = task.result! as AWSSNSCreateEndpointResponse
if let endpointArnForSNS = createEndpointResponse.endpointArn {
print("endpointArn: \(endpointArnForSNS)")
Settings.setPushArn(endpointArnForSNS)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "RegisteredForPush"), object: nil)
}
}
return nil
})
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let visible = window?.visibleViewController()
if let data = userInfo["aps"] as? [AnyHashable: Any] {
if let route = data["route"] as? String {
switch route {
case "matchRequested":
var projectId = ""
if let project = data["projectId"] as? String {
projectId = project
}
var matchId = ""
if let match = data["matchId"] as? String {
matchId = match
}
let projectMatches = MatchRequestedViewController(withNotificationPayload: projectId, matchId: matchId)
visible?.navigationController?.pushViewController(projectMatches, animated: true)
case "projectDetails":
var projectId = ""
if let project = data["projectId"] as? String {
projectId = project
}
let projectMatches = ProjectDetailsViewController(withProject: TERMyProject(), orProjectId: projectId)
visible?.navigationController?.pushViewController(projectMatches, animated: true)
case "matched":
var projectId = ""
if let project = data["projectId"] as? String {
projectId = project
}
var matchId = ""
if let match = data["matchId"] as? String {
matchId = match
}
var originProject: TERMyProject = TERMyProject()
var matchedProject: TERMatchedProject = TERMatchedProject()
AppController.sharedInstance.AWSClient?.projectsGet(id:projectId).continueWith(block: { (task: AWSTask) -> Any? in
if let error = task.error {
print("Error: \(error)")
} else if let result = task.result {
if result is NSDictionary {
DispatchQueue.main.async {
let array = [result]
let parsedProject: [TERMyProject] = TERMyProject.parseFromAPI(array:array as! [NSDictionary])
for project in parsedProject {
originProject = project
}
// self.initialSetup()
}
AppController.sharedInstance.AWSClient?.projectsGet(id:matchId).continueWith(block: { (task: AWSTask) -> Any? in
if let error = task.error {
print("Error: \(error)")
} else if let result = task.result {
if result is NSDictionary {
DispatchQueue.main.async {
let array = [result]
let parsedProject: [TERMatchedProject] = TERMatchedProject.parseFromAPI(array:array as! [NSDictionary])
for project in parsedProject {
matchedProject = project
}
let projectMatches = MatchedViewController(withProject: originProject, match: matchedProject, isComplete: false)
visible?.navigationController?.pushViewController(projectMatches, animated: true)
}
}
}
return nil
})
}
}
return nil
})
default:
break
}
}
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error.localizedDescription)
}
// Called when a notification is delivered to a foreground app.
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("User Info = ",notification.request.content.userInfo)
completionHandler([.alert, .badge, .sound])
}
//MARK: Boiler-plate methods
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
// func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
// if (self.navigationController == nil) {
//
// self.navigationController = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as? UINavigationController
// }
//
// if (self.loginViewController == nil) {
// self.loginViewController = self.navigationController?.viewControllers[0] as? LoginViewController
// }
//
// DispatchQueue.main.async {
// self.navigationController!.popToRootViewController(animated: true)
// if (!self.navigationController!.isViewLoaded
// || self.navigationController!.view.window == nil) {
// self.window?.rootViewController?.present(self.navigationController!,
// animated: true,
// completion: nil)
// }
//
// }
// return self.loginViewController!
// }
}
// MARK:- AWSCognitoIdentityRememberDevice protocol delegate
extension AppDelegate: AWSCognitoIdentityRememberDevice {
func didCompleteStepWithError(_ error: Error?) {
}
func getRememberDevice(_ rememberDeviceCompletionSource: AWSTaskCompletionSource<NSNumber>) {
}
}
extension UIWindow {
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
break;
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
break;
default:
if let presentedViewController = vc.presentedViewController {
//print(presentedViewController)
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
break;
}
}
}
AppController.swift
func launchInWindow(aWindow: UIWindow?){
self.window = aWindow
self.initializeSDKs()
self.globalCustomization()
self.AWSUnAuthedClient.apiKey = Constants.APIKeys.AWSAPIKey
self.window!.rootViewController = self.showLoggedInStateAndReturn(true)
self.window!.makeKeyAndVisible()
}
func initializeSDKs() {
// Google places
GMSPlacesClient.provideAPIKey(Constants.APIKeys.GooglePlaces)
}
func globalCustomization() {
self.styleNavigationBar()
}
#discardableResult func showLoggedInStateAndReturn(_ shouldReturn: Bool) -> UIViewController? {
//AppController.sharedInstance.enableCognitoClientWithAuthentication()
//self.registerForPush()
self.tabBarController = ESTabBarController()
//tabBarController.delegate = delegate
self.tabBarController?.title = "Irregularity"
self.tabBarController?.tabBar.shadowImage = UIImage.image(with: UIColor("FFFFFF", alpha: 0.0)!)
self.tabBarController?.tabBar.backgroundImage = UIImage.image(with: UIColor("2A2A27")!)
self.tabBarController?.shouldHijackHandler = {
tabbarController, viewController, index in
if index == 1 {
return true
}
return false
}
self.tabBarController?.didHijackHandler = {
[weak tabBarController] tabbarController, viewController, index in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let newProjectNavCon = UINavigationController(rootViewController: NewProjectViewController())
newProjectNavCon.hero.isEnabled = true
newProjectNavCon.setNavigationBarHidden(true, animated: false)
newProjectNavCon.hero.navigationAnimationType = .fade
tabBarController?.present(newProjectNavCon, animated: true, completion: nil)
}
}
let centerVC = UINavigationController(rootViewController: HomeViewController())
let v1 = centerVC
let v2 = BaseViewController()
let v3 = UINavigationController(rootViewController: ProfileViewController())
v1.tabBarItem = ESTabBarItem.init(TabBarBouncesContentView(), title: "Projects", image: UIImage(named: "tabBar"), selectedImage: UIImage(named: "tabBar"))
v2.tabBarItem = ESTabBarItem.init(TabBarIrregularityContentView(), title: nil, image: UIImage(named: "tabBarPlusButton"), selectedImage: UIImage(named: "tabBarPlusButton"))
v3.tabBarItem = ESTabBarItem.init(TabBarBouncesContentView(), title: "Profile", image: UIImage(named: "tabBarProfile"), selectedImage: UIImage(named: "tabBarProfile"))
self.tabBarController?.setViewControllers([v1, v2, v3], animated: true)
if shouldReturn {
return self.tabBarController
} else {
self.window?.rootViewController = self.tabBarController
return nil
}
}
You should try to execute the piece of code in the main thread:
func launchInWindow(aWindow: UIWindow?){
self.window = aWindow
self.initializeSDKs()
self.globalCustomization()
self.AWSUnAuthedClient.apiKey = Constants.APIKeys.AWSAPIKey
DispatchQueue.main.async {
self.window!.rootViewController = self.showLoggedInStateAndReturn(true)
self.window!.makeKeyAndVisible()
}
}
Also, your question codebase looks a little bit weird in this part in AppDelegate.swift:
self.window = UIWindow(frame: UIScreen.main.bounds) //-- Purple warning here
AppController.sharedInstance.launchInWindow(aWindow: self.window)
return true
Looks like those three lines are not in a function but in class scope, which is weird and you shouldn't be able to compile it. Probably you pasted your code wrong?

Update tabbarcontroller badge from a static struct

I'm not sure if this is possible or if I'm going about this the wrong way but...
I have an ecommerce app. You can add products to your cart. I'd like to display a badge over cart with the item quantity in the cart.
However, my Cart is in a static struct which needs to update the tabbarController.
How can I get these two to communicate between each other?
Cart
import Foundation
struct Cart {
static var items: [CartItem] = []
static func updateItem(id:Int,increment:Int){
var itemAdded = false
for (index, item) in items.enumerated() {
if item.fabric.id == id {
item.quantity = item.quantity+increment
if item.quantity <= 0 {
items.remove(at: index)
}
itemAdded = true
}
}
if !itemAdded {
// fabric not in cart
// add it
for item in API.fabrics{
if item.id == id{
items.append(CartItem(fabric: item, quantity: 1))
return
}
}
}
// AREA IN QUESTION
// let MainTabBarController = AppDelegate
// if let tabItems = tabBarController?.tabBar.items{
//
// }
}
}
class CartItem{
let fabric: Fabric
var quantity: Int
init(fabric: Fabric,
quantity: Int){
self.fabric = fabric
self.quantity = quantity
}
}
app delegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UIApplication.shared.statusBarStyle = .lightContent
window = UIWindow(frame: UIScreen.main.bounds)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let MainTabBarController = storyboard.instantiateViewController(withIdentifier: "MainTabBarController")
window?.rootViewController = MainTabBarController
window?.makeKeyAndVisible()
return true
}
You can use NSNotificationcenter.post(_ notification: Notification), so everytime you add an item to your cart you post a notification, and the your TabController can subscribe to this notification and handle it. Something like:
import NotificationCenter
import UIKit
struct Cart {
func add(item: Int) {
// Your code to handle stuff...
let numberOfItems = item
let notification = Notification(name: Notification.Name(rawValue: "addItem"), object: nil, userInfo: ["numberOfItems":numberOfItems])
NotificationCenter.default.post(notification)
}
}
class YourTabBarController: UITabBarController {
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(handleAddItem), name: NSNotification.Name(rawValue: "addItem"), object: nil)
}
#objc func handleAddItem(notification: Notification) {
// increment badge value
tabBar.items?.first?.badgeValue = "\(notification.userInfo!["numberOfItems"]!)"
}
}
Of course, you can do the same thing to remove items and every other stuff you need.
If tabbarcontroller is the rootviewcontroller
if let tabbarController = UIApplication.shared.keyWindow?.rootViewController as? MainTabBarController{
tabbarControllertabBar.items[indexpOfTabbarItem]!.badgeValue = value
}
if rootviewcontroller is navigationcontroller of tabbarcontroller:
if let tabbarNavigationController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController, let tabbarController = tabbarNavigationController.viewControllers.first as? MainTabBarController {
tabbarControllertabBar.items[indexpOfTabbarItem]!.badgeValue = value
}

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0) When working in swift trying to login to spotify

Code:
//
// AppDelegate.swift
// SplitterSwift3
//
// Created by VideoLabN on 4/8/18.
// Copyright © 2018 VideoLabN. All rights reserved.
//
import UIKit
import AWSAuthCore
import AWSMobileClient
import AWSCore
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var auth = SPTAuth()
// Add a AWSMobileClient call in application:open url
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
// called when user signs into spotify. Session data saved into user defaults, then notification posted to call updateAfterFirstLogin in ViewController.swift. Modeled off recommneded auth flow suggested by Spotify documentation
if auth.canHandle(auth.redirectURL) {
auth.handleAuthCallback(withTriggeredAuthURL: url, callback: { (error, session) in
if error != nil {
print("error!")
}
let userDefaults = UserDefaults.standard
let sessionData = NSKeyedArchiver.archivedData(withRootObject: session)
print(sessionData)
userDefaults.set(sessionData, forKey: "SpotifySession")
userDefaults.synchronize()
NotificationCenter.default.post(name: Notification.Name(rawValue: "loginSuccessfull"), object: nil)
})
return true
}
return false
}
//Add a AWSMobileClient call in application:didFinishLaunching
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return AWSMobileClient.sharedInstance().interceptApplication(
application, didFinishLaunchingWithOptions:
launchOptions)
}
}
//
// ViewController.swift
// SplitterSwift3
//
// Created by VideoLabN on 4/8/18.
// Copyright © 2018 VideoLabN. All rights reserved.
//
import UIKit
import SafariServices
import AVFoundation
import AWSAuthCore
import AWSAuthUI
class ViewController: UIViewController, SPTAudioStreamingPlaybackDelegate, SPTAudioStreamingDelegate {
// Variables
var auth = SPTAuth.defaultInstance()!
var session:SPTSession!
// Initialzed in either updateAfterFirstLogin: (if first time login) or in viewDidLoad (when there is a check for a session object in User Defaults
var player: SPTAudioStreamingController?
var loginUrl: URL?
// Outlets
#IBOutlet weak var loginSpotify: UIButton!
#IBOutlet weak var loginSplitter: UIButton!
#IBOutlet weak var testLabel: UILabel!
override func viewDidLoad() {
print("test")
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.setup()
//NotificationCenter.default.addObserver(self, selector: #selector(ViewController.updateAfterFirstLogin, name: NSNotification.Name(rawValue: "loginSuccessfull"), object: nil)
//self.updateAfterFirstLogin()
}
func setup () {
// insert redirect your url and client ID below
let redirectURL = "splitter-app://callback" // put your redirect URL here
let clientID = "207ce42c908f42e485c540be11720888" // put your client ID here
auth.redirectURL = URL(string: redirectURL)
auth.clientID = "client id goes here"
auth.requestedScopes = [SPTAuthStreamingScope, SPTAuthPlaylistReadPrivateScope, SPTAuthPlaylistModifyPublicScope, SPTAuthPlaylistModifyPrivateScope]
loginUrl = auth.spotifyWebAuthenticationURL()
//print("test")
}
func initializePlayer(authSession:SPTSession){
if self.player == nil {
self.player = SPTAudioStreamingController.sharedInstance()
self.player!.playbackDelegate = self
self.player!.delegate = self
try! player!.start(withClientId: auth.clientID)
self.player!.login(withAccessToken: authSession.accessToken)
}
}
#objc func updateAfterFirstLogin () {
loginSpotify.isHidden = true
let userDefaults = UserDefaults.standard
if let sessionObj:AnyObject = userDefaults.object(forKey: "SpotifySession") as AnyObject? {
let sessionDataObj = sessionObj as! Data
let firstTimeSession = NSKeyedUnarchiver.unarchiveObject(with: sessionDataObj) as! SPTSession
self.session = firstTimeSession
initializePlayer(authSession: session)
}
}
func audioStreamingDidLogin(_ audioStreaming: SPTAudioStreamingController!) {
// after a user authenticates a session, the SPTAudioStreamingController is then initialized and this method called
print("logged in")
//
self.player?.playSpotifyURI("spotify:track:58s6EuEYJdlb0kO7awm3Vp",
startingWith: 0, startingWithPosition: 0, callback: { (error) in
// if (error != nil) {
// print("playing!")
// }
//
// })
}
#IBAction func spotifyButtonPressed(_ sender: Any) {
let svc = SFSafariViewController(url: loginUrl!)
self.present(svc, animated: true, completion: nil)
//UIApplication.shared.open(loginUrl!, options: [:])
}
}
The application compiles just fine and launches on the simulator IPhone. The button to login to Spotify works, and opens a Safari instance prompting the user to login to Spotify.
Once the user has logged in, it then asks for permissions. Once the user accepts the permissions, the app crashes on line 16 of the appDelegate class with this error:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x0).
I have read up on this online and people are saying it is the equivalent to a null pointer exception, but I cannot find what is causing this error. Can anyone find the error?
Edit: Thanks to those that have responded! Here is my console output:
objc[19082]: Class VCWeakObjectHolder is implemented in both
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/AVConference.framework/Frameworks/ViceroyTrace.framework/ViceroyTrace
(0x12b9174d0) and
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/AVConference.framework/AVConference
(0x12aa65e38). One of the two will be used. Which one is undefined.
test 2018-04-15 13:33:50.341600-0400 SplitterSwift3[19082:1059086]
[AXRun-PID] Client requesting unsuspension of PID:-1 Name:
2018-04-15 13:33:50.441522-0400 SplitterSwift3[19082:1058985] [MC]
System group container for systemgroup.com.apple.configurationprofiles
path is
/Users/videolabn/Library/Developer/CoreSimulator/Devices/CEC32A65-63E0-4499-AB25-6BD13A7AE013/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles
2018-04-15 13:33:50.442782-0400 SplitterSwift3[19082:1058985] [MC]
Reading from private effective user settings. 2018-04-15
13:33:50.536744-0400 SplitterSwift3[19082:1058985] [App] if we're in
the real pre-commit handler we can't actually add any new fences due
to CA restriction (lldb)
The UIApplicationDelegate function application(_:open:sourceApplication:annotation:) was deprecated in iOS 10.
As of iOS 11.3 it seems that apps using this function will crash when it is invoked. The solution is is to use the replacement application(_:open:options:) instead.
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
if auth.canHandle(auth.redirectURL) {
auth.handleAuthCallback(withTriggeredAuthURL: url, callback: { (error, session) in
if error != nil {
print("error!")
}
let userDefaults = UserDefaults.standard
let sessionData = NSKeyedArchiver.archivedData(withRootObject: session)
print(sessionData)
userDefaults.set(sessionData, forKey: "SpotifySession")
NotificationCenter.default.post(name: Notification.Name(rawValue: "loginSuccessfull"), object: nil)
})
return true
}
return false
}
Also, there is no need to call synchronize for UserDefaults

Using NotificationCenter Observer to Handle Asynchronous Requests

Similar questions to this have been asked so I apologize, but none of them have been able to help me.
I am struggling to return the value from this asynchronous request to Firebase with a completion handler. The value I am retrieving from Firebase is an array and it does exist. But
Here is my function for making the request to Firebase:
class SearchManager {
var searchResults = [String]()
var listOfMosaics = [String]()
// Retrieves company list from Firebase
func getMosaicTitles(completionHandler: #escaping (_ mosaics: [String]) -> ()) {
Database.database().reference().child("mosaics").observeSingleEvent(of: .value, with: { (snapshot) in
guard let allMosaics = snapshot.value as? [String] else {
print("unable to unwrapp datasnapshot")
return
}
completionHandler(allMosaics)
})
}
// resets search results array
func resetSearch() {
searchResults = []
}
// takes list of all mosaics and filters based on search text
func filterAllMosaics(searchText: String) {
searchResults = listOfMosaics.filter { $0.contains(searchText) }
}
}
And in the AppDelegate I call it like this posting a Notification:
let searchManager = SearchManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
makeRootViewLaunchScreen()
FirebaseApp.configure()
searchManager.getMosaicTitles { (results) in
self.searchManager.listOfMosaics = results
NotificationCenter.default.post(name: NSNotification.Name("mosaicsReturned"), object: nil)
self.stopDisplayingLaunchScreen()
}
// Adds border to bottom of the nav bar
UINavigationBar.appearance().shadowImage = UIImage.imageWithColor(color: UIColor(red:0.00, green:0.87, blue:0.39, alpha:1.0))
// Override point for customization after application launch.
return true
}
func makeRootViewLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "launchScreen")
UIApplication.shared.keyWindow?.rootViewController = viewController
}
// reassigns root view after Firebase request complete
func stopDisplayingLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "centralViewController")
UIApplication.shared.keyWindow?.rootViewController = viewController
}
In the viewDidLoad of the viewController that supports the tableView that uses the retrieved array to populate it I add a Notification Observer.
var listOfMosaics = [String]()
var searchResults = [String]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
listOfMosaics = searchManager.listOfMosaics
configureSearchBar()
configureSearchBarTextField()
self.tableView.separatorColor = UIColor(red:0.00, green:0.87, blue:0.39, alpha:1.0)
NotificationCenter.default.addObserver(self, selector: #selector(updateListOfMosaics), name: NSNotification.Name("mosaicsReturned"), object: nil)
}
#objc func updateListOfMosaics(notification: Notification) {
listOfMosaics = searchManager.listOfMosaics
}
But when I call the below code it doesn't work the arrays print as empty and as a result it doesn't update my tableView.
extension SearchResultsTableViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchManager.resetSearch()
searchManager.filterAllMosaics(searchText: searchBar.text!)
tableView.reloadData()
print(listOfMosaics)
print(searchResults)
}
}
Thank you in advanced for the help.
This should work for you now. I think you didn't pass the instance of SearchManager from your AppDelegate to your ViewController. I'm guessing you created a new instance of SearchManager in your ViewController, which has an empty array.
Search Manager:
class SearchManager {
var searchResults = [String]()
var listOfMosaics = [String]()
func getMosaicTitles(completionHandler: #escaping (_ mosaics: [String]) -> ()) {
Database.database().reference().child("mosaics").observeSingleEvent(of: .value, with: { (snapshot) in
guard let allMosaics = snapshot.value as? [String] else {
print("unable to unwrapp datasnapshot")
completionHandler([]) // <- You should include this too.
return
}
completionHandler(allMosaics)
})
}
func resetSearch() {
searchResults = []
}
func filterAllMosaics(searchText: String) {
searchResults = listOfMosaics.filter { $0.contains(searchText) }
}
}
View Controller:
class TableViewController: UITableViewController {
var searchManager: SearchManager?
var listOfMosaics = [String]()
var searchResults = [String]() {
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
guard let searchManager = searchManager else { return }
listOfMosaics = searchManager.listOfMosaics
print("List of mosaics: \(listOfMosaics)")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
}
AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let searchManager = SearchManager()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
makeRootViewLaunchScreen()
FirebaseApp.configure()
searchManager.getMosaicTitles { results in
self.searchManager.listOfMosaics = results
self.stopDisplayingLaunchScreen()
}
return true
}
func makeRootViewLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "LaunchScreen", bundle: nil)
let viewController = mainStoryboard.instantiateViewController(withIdentifier: "launchScreen")
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
func stopDisplayingLaunchScreen() {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
guard let viewController = mainStoryboard.instantiateViewController(withIdentifier: "centralViewController") as? TableViewController else { return }
let navigationController = UINavigationController(rootViewController: viewController)
viewController.searchManager = searchManager
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
}
As #TNguyen says in his comment, it sounds like you aren't waiting for the async function getMosaicTitles() to complete.
You might want to disable the search bar button while the call is running, and enable it from the completion handler once the call is complete. Then the user won't be able to click the search button until the results have finished loading.
You can fetch the data from the database in a background thread and add a completion block, so that the tableView reloads only after the updated content is fetched.

My code for NSUserDefaults is not working

I am very new to swift. And need your help!
I want that, when the user logs in for the second time , the app should directly take it to the next view controller named CoreView. It should not ask for details, but I don't know why its not working. And it's asking for details everytime the app is launched. Please check the below code. I am not getting any sort of error too. Unless and until the app is killed or logged out, the user should be able to log in directly .
func pref_write()
{
// To write the data to NSUserDefaults
let prefs = NSUserDefaults.standardUserDefaults() // make a reference
print("OTP:\(OTP)")
// Adding values. Creating objects in prefs
prefs.setObject(OTP, forKey: "OTP")
print("check_OTP:\(check_OTP)")
prefs.setObject(U_ID, forKey: "U_ID")
print("Check_U_ID:\(check_U_ID)")
prefs.synchronize()
self.performSegueWithIdentifier("ContinueToCoreView", sender: self)
}
And in the viewDidLoad function:
override func viewDidLoad()
{
super.viewDidLoad()
//Read the data
self.performSegueWithIdentifier("ContinueToCoreView", sender: self)
pref_write()
let prefs = NSUserDefaults.standardUserDefaults()
check_OTP = prefs.objectForKey("OTP")!
check_U_ID = prefs.objectForKey("U_ID")!
prefs.objectForKey("U_ID")
print("prefs:\(prefs)")
prefs.synchronize()
}
Thanks!
Create a class as
class User_Details : NSObject
{
var user_id : String?
var user_otp : String?
var otp_verified : Bool?
init(u_id:String, otp:String?, verified:Bool)
{
super.init()
self.user_id = u_id
self.otp_verified = verified
self.user_otp = otp
}
}
In AppDelegate,
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
navController = self.window?.rootViewController as? UINavigationController
if self.checkIfUserLoggedIn()
{
let user_details = NSUserDefaults.standardUserDefaults().objectForKey("user_details") as! User_Details
self.moveToNextScreen(user_details)
}
return true
}
//AppDelegate Class or in the class which is globally accessible
func pref_write_user(user_details : User_Details)
{
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setObject(user_details, forKey: "user_details")
prefs.setBool(true, forKey: "is_user_login")
//After saving the OTP for current user, check for otp verified, move to OTP Screen
self.moveToNextScreen(user_details)
}
func moveToNextScreen(user_details : User_Details)
{
if user_details.otp_verified == false
{
// Move to OTP screen
let viewController = self.navController?.storyboard?.instantiateViewControllerWithIdentifier("otpScreen")
self.navController?.pushViewController(viewController!, animated: false)
}
else // Move to Home Screen
{
let viewController = self.navController?.storyboard?.instantiateViewControllerWithIdentifier("homeScreen")
self.navController?.pushViewController(viewController!, animated: false)
}
}
func logoutUser()
{
let prefs = NSUserDefaults.standardUserDefaults()
prefs.setObject(nil, forKey: "user_details")
prefs.setBool(false, forKey: "is_user_login")
}
func checkIfUserLoggedIn() -> Bool
{
let prefs = NSUserDefaults.standardUserDefaults()
if prefs.boolForKey("is_user_login")
{
if let _ = prefs.objectForKey("user_details")
{
return true
}
else
{
//User details not found for some reason, so setting the inital values and return false
self.logoutUser()
}
}
return false
}
Login Class :
Call the API for login by providing the basic credential, get the user_id and user_otp, save them to NSUserDefaults
func requestLoginToServer()
{
//Perform basic server action
....
//In Success Block write this
let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
// pass the values as return by the server
let user_details = User_Details(u_id: "123", otp: "1234", verified: false)
appDel.pref_write_user(user_details)
appDel.moveToNextScreen(user_details)
}
Please try this way. I just rearranged your code.
First it will check the login credentials with in the didload method of initial view controller. If it not there it will call the method pref_write() . Please make sure that the values used in pref_write() method are not nil
override func viewDidLoad()
{
super.viewDidLoad()
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
// You can give conditions for your need like if(prefs.valueForKey("U_ID") != nil))
// It will check the user defaults whether you already login
if(prefs.valueForKey("OTP") != nil) {
self.performSegueWithIdentifier("ContinueToCoreView", sender: self)
}
else{
pref_write()
}
}
// Make sure the Values are not nil
func pref_write()
{
// To write the data to NSUserDefaults
let prefs = NSUserDefaults.standardUserDefaults() // make a reference
print("OTP:\(OTP)")
// Adding values. Creating objects in prefs
prefs.setObject(OTP, forKey: "OTP")
print("check_OTP:\(check_OTP)")
prefs.setObject(U_ID, forKey: "U_ID")
print("Check_U_ID:\(check_U_ID)")
prefs.synchronize()
self.performSegueWithIdentifier("ContinueToCoreView", sender: self)
}
Hope its working...

Resources