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
}
Related
I'm trying to open another controller by tapping on the cell of my tableView. I'm coding with MVVM and Coordinator pattern.
In the beginning we see this screen - it is declarated in the method start()
let service = Service()
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel(service: service)
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
Later, my goal is to open all list of countries of the continent, but now l just need to open empty ViewController by tap on the cell (ex. Africa or Antarctica). Here is my methods for it, but they don't work.
private func showCountries() {
let vc = ViewController()
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel
.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries() // don't work
// print("show \(continent)") // work - continent is a param of .onContinentTap, which prints an geo-id of the continent, just if you need to know.
}
}
.disposed(by: viewModel.bag)
}
Thank you so much!
The following works as expected. What are you doing differently?
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var viewModel: ViewModel?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
viewModel = ViewModel()
let controller = viewModel?.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = controller
window?.makeKeyAndVisible()
return true
}
}
final class ViewModel {
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel()
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
private func showCountries() {
let vc = UIViewController()
vc.view.backgroundColor = .blue
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries()
}
}
.disposed(by: viewModel.bag)
}
}
final class ContinentsViewModel {
enum Flow {
case onContinentTap
}
let flow: Observable<Flow>
let bag = DisposeBag()
init() {
flow = .just(.onContinentTap)
.delay(.seconds(3), scheduler: MainScheduler.instance)
}
}
final class ContinentsViewController: UIViewController {
var viewModel: ContinentsViewModel
init(viewModel: ContinentsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
Tell me, please, what am I doing wrong?
There are 3 View:
The user selects his gender
The user enters his weight
Main View Application
After the user has written this data, he gets to the main screen of the application. I want to make the user choose his gender and weight only at the very first launch of the application. To do this, I created a UserSettings model to store the entered values there:
final class UserSettings {
enum SettingsKeys: String {
case userSex
case userWeight
}
static var userSex: String! {
get {
return UserDefaults.standard.string(forKey: SettingsKeys.userSex.rawValue)
}
set {
let defaults = UserDefaults.standard
let key = SettingsKeys.userSex.rawValue
if let sex = newValue {
print("Пол \(sex) добавлен в \(key)")
defaults.set(sex, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}
static var userWeight: String! {
get {
return UserDefaults.standard.string(forKey: SettingsKeys.userWeight.rawValue)
}
set {
let defaults = UserDefaults.standard
let key = SettingsKeys.userWeight.rawValue
if let weight = newValue {
print("Вес \(weight) добавлен в \(key)")
defaults.set(weight, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
}
}
In AppDelegate in function didFinishLaunchingWithOptions launchOptions wrote:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let defaults = UserDefaults.standard
let skipPageSex = defaults.bool(forKey: UserSettings.userSex)
let skipPageWeight = defaults.bool(forKey: UserSettings.userWeight)
if skipPageSex && skipPageWeight == true {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let nextView: MainViewController = mainStoryboard.instantiateViewController(identifier: "MainViewController") as! MainViewController
window?.rootViewController = nextView
self.window?.makeKeyAndVisible()
} else {
let firstStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let firstView: WelcomeViewController = firstStoryboard.instantiateViewController(identifier: "welcomeVC") as! WelcomeViewController
window?.rootViewController = firstView
self.window?.makeKeyAndVisible()
}
return true
}
In the Sex select View in the button action, I wrote:
#IBAction func manSelected(_ sender: UIButton) {
sender.setTitle("Male", for: .normal)
UserSettings.userSex = sender.currentTitle
let defaults = UserDefaults.standard
defaults.setValue(true, forKey: UserSettings.userSex)
defaults.synchronize()
}
And the same for the button with the female choice, only sender.setTitle("Female", for: .normal)
I tried removing UIMainStoryboardFile and UIApplicationSceneManifest in Info.plist and in this case I get just a black screen without errors. In the Storyboard ID I use the same value as in AppDelegate. Please, help
Try something like this. Initial with a TempViewController. Depend on your data, it will segue perform show Gender Picker or go directly to Main. Remember to check the Is Initial View Controller for root Navigation Controller.
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()
}
}
my code:
public func start() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
guard let listVC = storyboard.instantiateViewController(withIdentifier: "ListVC") as? ListVC else { return }
let viewModel = ListViewModel(dependencies: appDependencies)
viewModel.delegate = self
listVC.listViewModel = viewModel
navigationController?.pushViewController(listVC, animated: true)
}
protocol ListViewModelDelegate: class {
func needChangeScreen(cellViewModel: UserCellViewModel)
}
final class ListViewModel {
weak var delegate: ListViewModelDelegate?
func userPressed(at index: IndexPath) {
delegate?.needChangeScreen(cellViewModel: cellViewModels[index.row])
}
}
User pressed is called from UIViewController , then i want to send callback to Coordinator to start another coordinator but delegate? is always nil. I know that delegates should be weak but in this case is not working for me. Any ideas?
Okey i have fix but i do not know is it good.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let navigationController = UINavigationController()
navigationController.setNavigationBarHidden(true, animated: false)
window?.rootViewController = navigationController
let appDependencies = configureAppDependencies()
let coordinator = AppCoordinator(navigationController: navigationController, appDependencies: appDependencies)
coordinator.start()
window?.makeKeyAndVisible()
return true
}
That was my app delegate function(In AppCoordinator start is created and pushed ListCoordinator) but when i changed let coordinator for instance var :
var coordinator: AppCoordinator?
weak delegate is not nil and everything works.
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.