iOS Swift fetching more data for collection view after having navigated to detail view not working - ios

I am working on an assignment for a job interview. I have finished most of the assignment. There's only a bug i can't figure out. I have been trying for three days now.
I had to make a client app for the Flickr API that allows users to search for photos using specific words. Display the results in a collection view with infinite scroll. And when a photo is selected it should show the details of the photo in a detail view.
The bug:
Everything is working if i stay in the collection view. I can search over and over again and the infinite scroll is also working.As soon as a specific index in the index path is hit. A new request is sent with the same search term. But if i select a photo and then navigate back to the collection view and try a new search nothing comes back and my error handeling returns an error. (the error is not a console error).Also when navigating back from detail to collection. I can still scroll until the index triggers a new request than it also throws an error.
I hope i am explaining it well. I am really getting desperate at the moment. I tried everything i could think of: the request url still works when i try it in the browser.
Please help! If you need more info just ask.
Collection view controller:
import UIKit
// Global variable for holding a search term.
var searchTerm: String?
// Global variable to hold an instance of Reachability.
var reachability: Reachability?
// Enum for changing the textfield placeholder text.
enum TextFieldPlaceHolderText: String {
case Search = "Search"
case Searching = "Searching..."
}
class PhotosViewController: UIViewController {
// MARK: - Outlets
#IBOutlet var collectionView: UICollectionView!
#IBOutlet var searchTextField: UITextField!
// MARK: - Properties
let photoDataSource = PhotoDataSource()
let photoStore = PhotoStore()
// MARK: - View Setup
override func viewDidLoad() {
super.viewDidLoad()
// Sets the data source and delegate.
collectionView.dataSource = photoDataSource
collectionView.delegate = self
// Uses an image to add a pattern to the collection view background.
collectionView.backgroundColor = UIColor(patternImage: UIImage(named: "flickr.png")!)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Checks if the device is connected to the internet.
checkForReachability()
}
// MARK: showAlert
func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
let okAction = UIAlertAction(title: "Ok", style: .Cancel, handler: { (nil) in
self.dismissViewControllerAnimated(true, completion: nil)
})
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
// MARK: - Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowPhoto" {
if let selectedIndexPath = collectionView.indexPathsForSelectedItems()?.first {
let flickrPhoto = photoDataSource.flickrPhotos[selectedIndexPath.row]
let destinationVC = segue.destinationViewController as! PhotoDetailViewController
destinationVC.flickrPhoto = flickrPhoto
destinationVC.photoStore = photoStore
}
}
}
// MARK: - checkForReachability
func checkForReachability() {
do {
reachability = try Reachability.reachabilityForInternetConnection()
} catch {
print("Unable to create Reachability")
return
}
reachability!.whenReachable = { reachability in
// This is called on a background thread, but UI updates must be on the main thread.
NSOperationQueue.mainQueue().addOperationWithBlock({
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
})
}
reachability!.whenUnreachable = { reachability in
// This is called on a background thread, but UI updates must be on the main thread.
NSOperationQueue.mainQueue().addOperationWithBlock({
print("Not reachable")
self.showAlert("No Internet Connection", message: "Make sure your device is connected to the internet.")
})
}
do {
try reachability!.startNotifier()
} catch {
print("Unable to start notifier")
}
}
}
//MARK: - Extension UICollectionViewDelegate
extension PhotosViewController: UICollectionViewDelegate {
//MARK: - willDisplayCell
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
let flickrPhoto = photoDataSource.flickrPhotos[indexPath.row]
// Downloads the image data for a thumbnail.
photoStore.fetchImageForPhoto(flickrPhoto,thumbnail: true) { (result) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
// The indexpath for the photo might have changed between the time the request started and finished, so find the most recent indeaxpath
let photoIndex = self.photoDataSource.flickrPhotos.indexOf(flickrPhoto)!
let photoIndexPath = NSIndexPath(forRow: photoIndex, inSection: 0)
// When the request finishes, only update the cell if it's still visible
if let cell = collectionView.cellForItemAtIndexPath(photoIndexPath) as? PhotoCollectionViewCell {
cell.updateWithImage(flickrPhoto.image)
}
}
}
}
}
//MARK: - Extension UITextFieldDelegate
extension PhotosViewController : UITextFieldDelegate {
func textFieldShouldReturn(textField: UITextField) -> Bool {
// Checks if the textfield is not empty.
if textField.text!.isEmpty {
self.showAlert("S😉rry", message: "No search term detected, please enter a search term.")
return false
}
else {
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
textField.addSubview(activityIndicator)
activityIndicator.frame = textField.bounds
activityIndicator.startAnimating()
textField.placeholder = TextFieldPlaceHolderText.Searching.rawValue
// Sets the text that the user typed as the value for the searchTerm property.
searchTerm = textField.text!
// Fetches the photos from flickr using the user's search term.
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
// Checks if photos were found using the search term.
if photos.count == 0 {
self.showAlert("S😞rry", message: "No images found matching your search for: \(searchTerm!), please try again.")
}
activityIndicator.removeFromSuperview()
textField.placeholder = TextFieldPlaceHolderText.Search.rawValue
// Sets the result to the data source array.
self.photoDataSource.flickrPhotos = photos
print("Successfully found \(photos.count) recent photos.")
case let .Failure(error):
self.checkForReachability()
activityIndicator.removeFromSuperview()
textField.placeholder = TextFieldPlaceHolderText.Search.rawValue
self.photoDataSource.flickrPhotos.removeAll()
self.showAlert("", message: "Something went wrong, please try again.")
print("Error fetching photo's for search term: \(searchTerm!), error: \(error)")
}
self.collectionView.reloadSections(NSIndexSet(index: 0))
}
}
textField.text = nil
textField.resignFirstResponder()
self.collectionView?.backgroundColor = UIColor.whiteColor()
return true
}
}
}
The detail view controller:
import UIKit
import Social
class PhotoDetailViewController: UIViewController {
// MARK: - Outlets
#IBOutlet var photoTitleLabel: UILabel!
#IBOutlet var photoIDLabel: UILabel!
#IBOutlet var dateTakenLabel: UILabel!
#IBOutlet var imageView: UIImageView!
// MARK: Properties
var flickrPhoto: FlickrPhoto!
var photoStore: PhotoStore!
let formatter = FlickrAPI.dateFormatter
// MARK: - View Setup
override func viewDidLoad() {
super.viewDidLoad()
//Downloads the image data for large image
photoStore.fetchImageForPhoto(flickrPhoto, thumbnail: false) { (result) -> Void in
switch result {
case let .Success(image):
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.imageView.image = image
}
case let .Failure(error):
print(" Error fetching detail image for photo: \(error)")
}
}
// Formats the date a shorte date that doesn't display the time
formatter.dateStyle = .MediumStyle
formatter.timeStyle = .NoStyle
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Checks if the device is connected to the internet.
checkForReachability()
// Configures the UI.
configureView()
}
// MARK: - checkForReachability
func checkForReachability() {
do {
reachability = try Reachability.reachabilityForInternetConnection()
} catch {
print("Unable to create Reachability")
return
}
reachability!.whenReachable = { reachability in
// this is called on a background thread, but UI updates must be on the main thread, like this:
NSOperationQueue.mainQueue().addOperationWithBlock({
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
})
}
reachability!.whenUnreachable = { reachability in
// this is called on a background thread, but UI updates must be on the main thread, like this:
NSOperationQueue.mainQueue().addOperationWithBlock({
print("Not reachable")
self.showAlert("No Internet Connection", message: "Make sure your device is connected to the internet.")
})
}
do {
try reachability!.startNotifier()
} catch {
print("Unable to start notifier")
}
}
// MARK: - configureView
func configureView() {
photoTitleLabel.text = flickrPhoto.title ?? "No title available"
photoIDLabel.text = flickrPhoto.photoID ?? "ID unknown"
dateTakenLabel.text = formatter.stringFromDate(flickrPhoto.dateTaken) ?? " Date unknown"
}
// MARK: - showShareOptions
#IBAction func showShareOptions(sender: AnyObject) {
// Configure an action sheet to show the sharing options.
let actionSheet = UIAlertController(title: "Share this photo", message: "", preferredStyle: UIAlertControllerStyle.ActionSheet)
let tweetAction = UIAlertAction(title: "Share on Twitter", style: UIAlertActionStyle.Default) { (action) -> Void in
// Check if sharing to Twitter is possible.
if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter) {
let twitterComposeVC = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
twitterComposeVC.addImage(self.imageView.image)
self.presentViewController(twitterComposeVC, animated: true, completion: nil)
}
else {
self.showAlert("Flickr Searcher", message: "You are not logged in to your Twitter account.")
}
}
// Configure a new action to share on Facebook.
let facebookPostAction = UIAlertAction(title: "Share on Facebook", style: UIAlertActionStyle.Default) { (action) -> Void in
if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter) {
let facebookComposeVC = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
facebookComposeVC.addImage(self.imageView.image)
self.presentViewController(facebookComposeVC, animated: true, completion: nil)
}
else {
self.showAlert("Flickr Searcher", message: "You are not logged in to your facebook account.")
}
}
// Configure a new action to show the UIActivityViewController
let moreAction = UIAlertAction(title: "More", style: UIAlertActionStyle.Default) { (action) -> Void in
let activityViewController = UIActivityViewController(activityItems: [self.imageView.image!], applicationActivities: nil)
self.presentViewController(activityViewController, animated: true, completion: nil)
}
let dismissAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Destructive) { (action) -> Void in
}
actionSheet.addAction(tweetAction)
actionSheet.addAction(facebookPostAction)
actionSheet.addAction(moreAction)
actionSheet.addAction(dismissAction)
presentViewController(actionSheet, animated: true, completion: nil)
}
// MARK: showAlert
func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
let okAction = UIAlertAction(title: "Ok", style: .Cancel, handler: { (nil) in
self.dismissViewControllerAnimated(true, completion: nil)
})
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
}
My data source:
import UIKit
class PhotoDataSource: NSObject, UICollectionViewDataSource {
//MARK: - Properties
// Array to store the Flickr Photos
var flickrPhotos = [FlickrPhoto]()
// An instance of photoStore.
var photoStore = PhotoStore()
// MARK: - numberOfItemsInSection
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return flickrPhotos.count
}
// MARK: - cellForItemAtIndexPath
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let identifier = "FlickrCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath) as! PhotoCollectionViewCell
let photo = flickrPhotos[indexPath.item]
cell.updateWithImage(photo.image)
print(indexPath.item)
// If you get close to the end of the collection, fetch more photo's.
if indexPath.item == flickrPhotos.count - 20 {
print("Detected the end of the collection")
// Fetch the next batch of photos.
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
print("Successfully found \(photos.count) recent photos.")
self.flickrPhotos.appendContentsOf(photos)
case let .Failure(error):
self.flickrPhotos.removeAll()
print("Error fetching more photos for search term \(error)")
}
collectionView.reloadSections(NSIndexSet(index: 0))
}
}
}
return cell
}
}
This is the method that throws the error. But only when navigated to the detail view first. Staying in the collection view the method gets call over and over with no problem:
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
print("Successfully found \(photos.count) recent photos.")
self.flickrPhotos.appendContentsOf(photos)
case let .Failure(error):
self.flickrPhotos.removeAll()
print("Error fetching more photos for search term \(error)")
}
collectionView.reloadSections(NSIndexSet(index: 0))
}
}

Related

Why am I getting a Unexpectedly found nil while implicitly unwrapping an Optional value in my Code?

I am getting a Unexpectedly found nil while implicitly unwrapping an Optional value in my Code error when I try to use another view controller to save a new task in a to do list. When I tap a button I open up the entry page which then has a text field where I can enter the text to then create a task item. Here is the code for the main view controller:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
#IBOutlet var tableView: UITableView!
private var tasks = [TaskItem]()
override func viewDidLoad() {
super.viewDidLoad()
getAllTasks()
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = tasks[indexPath.row].title
return cell
}
#IBAction func didTapNewTask(){
let viewContoller = storyboard?.instantiateViewController(identifier: "entry") as! EntryViewController
viewContoller.title = "New Task"
viewContoller.update = {
DispatchQueue.main.async {
self.getAllTasks()
}
}
navigationController?.pushViewController(viewContoller, animated: true)
}
//Core Data Functions
//Used to get all our tasks in our Core Data
func getAllTasks() {
do {
tasks = try context.fetch(TaskItem.fetchRequest())
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print("error getting all tasks \(error)")
}
}
//This is used to create a task, setting the properties to those in the parameters and then saving to our Core Data.
func createTask(title: String, notes: String, difficulty: Int32) {
let task = TaskItem(context: context)
task.title = title
task.notes = notes
task.difficulty = difficulty
task.dateCreated = Date()
do {
try context.save()
getAllTasks()
}
catch {
}
}
Here is the code for the entry view controller:
class EntryViewController: UIViewController, UITextFieldDelegate {
#IBOutlet var field: UITextField!
var update: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
field.delegate = self
// Do any additional setup after loading the view.
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
saveTask()
return true
}
#IBAction func saveTask(){
let vc = storyboard?.instantiateViewController(identifier: "tasks") as! ViewController
guard let text = field.text, !text.isEmpty else {
let alert = UIAlertController(title: "Error", message: "Please input a title" , preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Confirm", style: UIAlertAction.Style.default, handler: nil))
self.present(alert,animated: true,completion: nil)
return
}
vc.createTask(title: text, notes: "Hello", difficulty: 10)
update?()
navigationController?.popViewController(animated: true)
}
The app crashes once I click save the new task but then once I reload the app the task I just created is there.
#IBAction func saveTask(){
let vc = storyboard?.instantiateViewController(identifier: "tasks") as! ViewController
guard let text = field.text, !text.isEmpty else {
let alert = UIAlertController(title: "Error", message: "Please input a title" , preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Confirm", style: UIAlertAction.Style.default, handler: nil))
self.present(alert,animated: true,completion: nil)
return
}
vc.createTask(title: text, notes: "Hello", difficulty: 10)
update?()
navigationController?.popViewController(animated: true)
}
The first line of this method is the source of your problem. What you're doing here is making a new instance of the original view controller, not the instance you first came from.
This sort of works for a moment, because you then call createTask on that view controller to make your new task. That's fine, but that method then calls getAllTasks, which then dispatches to the main queue, which then calls reload data on your table.
But your table doesn't exist, because this is a new instance of the view controller which has never had it's view loaded. The table view is an implicitly unwrapped optional, but it's nil when you hit it here.
Your best solution is to pass in a block (like you have with update) to create a new task, and in that block call methods on the original view controller.

Get selected text from custom UIMenuItem in WKWebView

I am creating an iOS app that has a custom UIMenuItem. This new custom UIMenuItem shows up when text is selected in WKWebView. How do I get the selected text.
I followed instructions in https://stackoverflow.com/a/49761522/6828076 to create a custom UIMenuItem. It works fine, but I need the selected text that was used when the custom UIMenuItem was tapped. There are many posts about using UIPasteboard but the custom item does not copy the selected text into the UIPasteboard, so I am unable to retrieve it.
func setupCustomMenu() {
let customMenuItem = UIMenuItem(title: "Foo", action:
#selector(ViewController.transelateMenuTapped))
UIMenuController.shared.menuItems = [customMenuItem]
UIMenuController.shared.update()
}
#objc func transelateMenuTapped() {
let yay = //Need to retrieve the selected text here
let alertView = UIAlertController(title: "Yay!!", message: yay, preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: "cool", style: .default, handler: nil))
present(alertView, animated: true, completion: nil)
}
You can use Javascript for that.
Here's the code from the the answer you used, slightly altered to get the selected text by evaluating Javascript on the WKWebView:
import UIKit
import WebKit
class ViewController: UIViewController {
weak var webView: CustomMenuWebView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
prepareWebView()
}
#objc func translateMenuTapped(_ test: Any) {
webView.evaluateJavaScript("window.getSelection().toString()") { (test, error) in
guard let test = test as? String, error == nil else { return }
// ***** Here's the user's selected text *****
print(test)
}
}
}
private extension ViewController {
func prepareWebView() {
addWebViewToView()
loadWebViewContent()
setupCustomMenu()
}
func addWebViewToView() {
let webView = CustomMenuWebView(
frame: view.bounds, configuration: WKWebViewConfiguration())
view.addSubview(webView)
self.webView = webView
}
func loadWebViewContent() {
let url = URL(string: "https://www.google.com")
let request = URLRequest(url: url!)
webView.load(request)
}
func setupCustomMenu() {
let customMenuItem = UIMenuItem(
title: "Translate", action: #selector(ViewController.translateMenuTapped))
UIMenuController.shared.menuItems = [ customMenuItem ]
UIMenuController.shared.update()
}
}
class CustomMenuWebView: WKWebView {
// Turn off all other menu items
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}

Swift background mode for BLE iOS9

I want to improve the MPCRevisited project which is Chat app that using multi peer method. I'm using BLE to connect one device to another device (iPad and iPod) and send and receive the data. However, when I press home button to make background mode on one device, after 5 seconds, I can't send or receive the data.
image description here
I've already check all the thing in background modes, but still its not working at all.
import UIKit
import MultipeerConnectivity
class ParkBenchTimer {
let startTime:CFAbsoluteTime
var endTime:CFAbsoluteTime?
init() {
startTime = CFAbsoluteTimeGetCurrent()
}
func stop() -> CFAbsoluteTime {
endTime = CFAbsoluteTimeGetCurrent()
return duration!
}
var duration:CFAbsoluteTime? {
if let endTime = endTime {
return endTime - startTime
} else {
return nil
}
}
}
class ChatViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var chatTextField: UITextField!
#IBOutlet weak var chatTableView: UITableView!
var messagesArray: [[String : String]] = []
let mpcManager = MPCManager.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.chatTableView.delegate = self
self.chatTableView.dataSource = self
self.chatTableView.estimatedRowHeight = 60.0
self.chatTableView.rowHeight = UITableViewAutomaticDimension
self.chatTextField.delegate = self
self.mpcManager.messageRecievedDelegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: IBAction method implementation
#IBAction func endChat(sender: AnyObject) {
let messageDictionary: [String: String] = ["message": "_end_chat_"]
if self.mpcManager.sendData(dictionaryWithData: messageDictionary, toPeer: self.mpcManager.session.connectedPeers[0] as MCPeerID){
self.dismissViewControllerAnimated(true, completion: { () -> Void in
self.mpcManager.session.disconnect()
})
}
}
// MARK: UITableView related method implementation
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.messagesArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("idCell") else {
assert(true)
return UITableViewCell()
}
guard let currentMessage = self.messagesArray[safe: indexPath.row] else {
print(" ")
assert(true)
return UITableViewCell()
}
if let sender = currentMessage["sender"] {
var senderLabelText: String
var senderColor: UIColor
if sender == "self" {
senderLabelText = "I said:"
senderColor = UIColor.purpleColor()
} else {
senderLabelText = sender + " said:"
senderColor = UIColor.orangeColor()
}
cell.detailTextLabel?.text = senderLabelText
cell.detailTextLabel?.textColor = senderColor
}
if let message = currentMessage["message"] {
cell.textLabel?.text = message
}
return cell
}
// MARK: UITextFieldDelegate method implementation
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
guard let textFieldText = textField.text else {
assert(true)
return false
}
let messageDictionary: [String: String] = ["message": textFieldText]
guard let connectedPeer = self.mpcManager.session.connectedPeers[safe: 0] else {
print(" ")
assert(true)
return false
}
if self.mpcManager.sendData(dictionaryWithData: messageDictionary, toPeer: connectedPeer) {
let dictionary = ["sender": "self", "message": textFieldText]
self.messagesArray.append(dictionary)
self.updateTableview()
} else {
print("Could not send data")
}
textField.text = ""
return true
}
// MARK: Custom method implementation
func updateTableview(){
chatTableView.reloadData()
if self.chatTableView.contentSize.height > self.chatTableView.frame.size.height {
let indexPathToScrollTo = NSIndexPath(forRow: messagesArray.count - 1, inSection: 0)
self.chatTableView.scrollToRowAtIndexPath(indexPathToScrollTo, atScrollPosition: .Bottom, animated: true)
}
}
}
extension ChatViewController : MPCManagerRecievedMessageDelegate {
func managerRecievedData(data:NSData ,fromPeer:MCPeerID) {
// Convert the data (NSData) into a Dictionary object.
let dataDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [String : String]
// Check if there's an entry with the "message" key.
if let message = dataDictionary["message"] {
// Make sure that the message is other than "_end_chat_".
if message != "_end_chat_"{
// Create a new dictionary and set the sender and the received message to it.
let messageDictionary: [String: String] = ["sender": fromPeer.displayName, "message": message]
// Add this dictionary to the messagesArray array.
messagesArray.append(messageDictionary)
// Reload the tableview data and scroll to the bottom using the main thread.
self.updateTableview()
} else {
}
}
}
func managerDidRecievedMessage(message: String, fromPeer: MCPeerID) {
// Create a new dictionary and set the sender and the received message to it.
//let messageDictionary: [String: String] = ["sender": fromPeer.displayName, "message": message]
// Add this dictionary to the messagesArray array.
//messagesArray.append(messageDictionary)
// Reload the tableview data and scroll to the bottom using the main thread.
//self.updateTableview()
}
func managerDidEndChat(fromPeer:MCPeerID) {
// In this case an "_end_chat_" message was received.
// Show an alert view to the user.
let alert = UIAlertController(title: "", message: "\(fromPeer.displayName) ended this chat.", preferredStyle: UIAlertControllerStyle.Alert)
let doneAction: UIAlertAction = UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default) { (alertAction) -> Void in
self.mpcManager.session.disconnect()
self.dismissViewControllerAnimated(true, completion: nil)
}
alert.addAction(doneAction)
self.presentViewController(alert, animated: true, completion: nil)
}
}
This is my code.
Please help me if someone knows this problem. What I want to do is one device to keep sending the message and other device to become background and foreground back and forth.
Thank you.
Looking at some other StackOverflow posts (here and here), it seems like the Multipeer Connectivity Framework is not built to function in the background and your functionality will disappear after a couple minutes.
Bluetooth will function in the background, with the capabilities that you checked, but you will have to create your own messaging platform; even though Multipeer relies partially on Bluetooth, the capabilities are separate entities.

Update Table View in iOS (Swift)

I am making a cocktail iOS application.
I am adding strings to a tableview (an ingredient to the "cabinet"). The user enters an ingredient and then presses the button ADD. It successfully adds it to the Core Data but it does not appear right away. What am I doing wrong?
Below is my code, thanks!
ViewController:
import UIKit
import CoreData
class CabinetViewController: UIViewController, UITextFieldDelegate, UITableViewDataSource, UITableViewDelegate {
var ingredientArray = [String]()
var display = [String]()
var dbIngredients = [String]()
let ingredientFetch = NSFetchRequest(entityName: "Cabinet")
var fetchedIngredient = [Cabinet]()
#IBOutlet weak var TextUI: UITextField!
#IBOutlet weak var Button: UIButton!
#IBOutlet weak var TableView: UITableView!
let moc = DataController().managedObjectContext
override func viewDidLoad() {
super.viewDidLoad()
TextUI.delegate = self
TextUI.addTarget(self, action: "textFieldDidChange:", forControlEvents: UIControlEvents.EditingChanged)
TableView.delegate = self
TableView.dataSource = self
TableView.registerClass(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
// fetch Core Data
do{
fetchedIngredient = try moc.executeFetchRequest(ingredientFetch) as! [Cabinet]
} catch {
fatalError()
}
let postEndpoint: String = "http://www.thecocktaildb.com/api/json/v1/1/list.php?i=list"
guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on www.thecocktaildb.com")
print(error)
return
}
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error trying to convert data to JSON")
return
}
var count = 1
if let drinks = post["drinks"] as? [NSDictionary] {
for drink in drinks {
if let strIngredient = drink["strIngredient1"] as? String {
print(String(count) + ". " + strIngredient)
self.dbIngredients.append(strIngredient)
count++
}
}
}
})
task.resume()
TableView.reloadData()
}
func textFieldDidChange(textField: UITextField) {
search(self.TextUI.text!)
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
Button.addTarget(self, action: "buttonPressed:", forControlEvents: .TouchUpInside)
return true
}
func buttonPressed(sender: UIButton!) {
//ingredientArray.append(TextUI.text!)
let entity = NSEntityDescription.insertNewObjectForEntityForName("Cabinet", inManagedObjectContext: moc) as! Cabinet
entity.setValue(TextUI.text!, forKey: "ingredient")
do{
try moc.save()
}catch {
fatalError("failure to save context: \(error)")
}
showAlertButtonTapped(Button)
// dispatch_async(dispatch_get_main_queue(), { () -> Void in
// self.TableView.reloadData()
// })
}
#IBAction func showAlertButtonTapped(sender: UIButton) {
// create the alert
let alert = UIAlertController(title: "Added!", message: "You've added " + TextUI.text! + " to your cabinet", preferredStyle: UIAlertControllerStyle.Alert)
// add an action (button)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
// show the alert
self.presentViewController(alert, animated: true, completion: nil)
}
func search(str:String) {
display.removeAll(keepCapacity: false)
for s in dbIngredients{
if s.hasPrefix(str){
display.append(s)
print(s)
}
}
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return fetchedIngredient.capacity
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
do{
let fetchedIngredient = try moc.executeFetchRequest(ingredientFetch) as! [Cabinet]
cell.textLabel?.text = fetchedIngredient[indexPath.row].ingredient
} catch {
fatalError("bad things happened: \(error)")
}
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let alert = UIAlertController(title: "Remove " + fetchedIngredient[indexPath.row].ingredient!,
message: "No more " + fetchedIngredient[indexPath.row].ingredient! + " in your cabinet?",
preferredStyle: .Alert)
let deleteAction = UIAlertAction(title: "Remove",
style: .Default,
handler: { (action:UIAlertAction) -> Void in
self.fetchedIngredient.removeAtIndex(indexPath.row)
do{
let fetchedResults = try self.moc.executeFetchRequest(self.ingredientFetch)
if let result = fetchedResults[indexPath.row] as? NSManagedObject {
self.moc.deleteObject(result)
try self.moc.save()
}
}catch{
fatalError()
}
})
let cancelAction = UIAlertAction(title: "Cancel",
style: .Default) { (action: UIAlertAction) -> Void in
}
alert.addAction(cancelAction)
alert.addAction(deleteAction)
presentViewController(alert,
animated: true,
completion: nil)
TableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Since your problem isn't Core Data you need to use Table View beginUpdates and EndUpdates to insert the row. At the end of your buttonPressed function put this:
do{
fetchedIngredient = try moc.executeFetchRequest(ingredientFetch) as! [Cabinet]
self.tableView.beginUpdates()
let totalIngredients = fetchedIngredient.count
let newItemIndexPath = NSIndexPath(forRow: totalIngredients-1, inSection: 0)
self.tableView.insertRowsAtIndexPaths([newItemIndexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
self.tableView.endUpdates()
} catch {
fatalError()
}
On your number of rows in section:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedIngredient.count
}
And on the cell for row at index path
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = fetchedIngredient[indexPath.row].ingredient
return cell
}
There are a couple of problems with your code. Firstly, since you're fetching records into an array, calling reloadData will not have any impact unless you update the array. There is no automatic connection between adding a new core data record and your fetchedIngredient array.
There are a few ways to solve this, although the most common is probably to just refetch the records into the same array whenever core data is updated. Alternatively you can change your code to us NSFetchedResultsController instead of an array, which will automatically update the tableView when core data is updated (based on the predicate you provide it). This class provides the automatic connection to core data for you.
Another problem is that you are refetching the records in cellForRowAtIndexPath and didSelectRowAtIndexPath. This should not be done. Instead you should just be referring to the class-level fetchedIngredient array (or the NSFetchedResultsController if you choose to use that).
Furthermore, the call to dataTaskWithRequest runs in the background. It's not clear from the code how you're using it, but the fact that you have reloadData afterwards suggests it should impact the tableView. However because the task runs in the background, the completion handler will run after the table is reloaded. Therefore you should be calling reloadData inside the completion handler. And since it would then be running on another thread, you would have to dispatch it to the main queue, using:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}

Unexpected quirky behavior from socket.io in Swift

As per title, I'm having some trouble dealing with socket.io. It connects really well and accordingly in the first view controller but weird things happen when it comes to second controller.
Here's the code:
First Controller: I have declared some global variable for connection purposes between both view controller.
import UIKit
import SocketIOClientSwift
import SwiftyJSON
import CoreData
//declare some global variable
var patientCoreData = [NSManagedObject]()
var numberOfUsersExisting:Int = 0 //assign to 0 by default
var appUserData: Patient? //for specific user
var pSample: Array<Patient> = [] //for all user
//initiate socket globally
let socket = SocketIOClient(socketURL: "localhost:3000", options: [
"reconnects": true
])
func reportStatus(){
socket.on("connect") {data, ack in
print("Report status: View Controller connected")
socket.emit("click", "Client app connected")
}
}
func readDataFromSocket(completion: (data:AnyObject)-> ()){
socket.on("reply") {data, ack in
print("database replied")
completion(data: data)
}//socket
}//readDataFromSOCKET
func importData(){
reportStatus()
socket.connect()
readDataFromSocket(){ data in
let json = JSON(data)
let nou = json[0].count
if nou > 0 {
print("Test(1st VC): grabbing data from database")
for var i=0; i<nou; ++i{
numberOfUsersExisting = nou
pSample += [Patient(id: json[0][i]["ID"].intValue, name: json[0][i]["Name"].stringValue, gender: json[0][i]["Gender"].stringValue, mileage: json[0][i]["Mileage"].doubleValue)]
pSample.sortInPlace({$0.globalPatientMileAge < $1.globalPatientMileAge})
}
print("Successfully grabbed data")
}else{
print("No user in the database")
numberOfUsersExisting = 0
}
}//readDataFromSocket
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
print("First view appeared")
let prefs = NSUserDefaults.standardUserDefaults()
//if an user has logged in
let isLoggedIn = prefs.integerForKey("ISLOGGEDIN") as Int
if (isLoggedIn != 1){
print("No user currently, so heading to login screen")
socket.disconnect()
self.performSegueWithIdentifier("gotoLogin", sender: self)
}else{
print("ViewDidAppear: An user has been logged in")
let permissionToLoadData = prefs.integerForKey("ISLOGGEDIN")
if (permissionToLoadData != 1) {
print("Please grant permission to get data")
}else{
print("First view: connecting to database")
importData()
}//permission to load data
}
}//end of viewDidAppear
}
Second Controller:
import UIKit
import SocketIOClientSwift
import SwiftyJSON
import CoreData
var nou:Int?
class LoginViewController: UIViewController {
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let registeredUserID = NSUserDefaults.standardUserDefaults().stringForKey("registerPatientID")
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
func displayAlertMessage(userMessage:String){
let alert = UIAlertController(title: "Alert", message: userMessage, preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
func successMessage(userMessage:String){
let alert = UIAlertController(title: "Welcome Back", message: userMessage, preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
#IBOutlet weak var loginPatientID: UITextField!
#IBAction func LoginButton(sender: AnyObject) {
let logInUserID = loginPatientID.text
if (logInUserID!.isEmpty){
displayAlertMessage("Please enter your Patient ID!")
return
}else{
print("Test: requesting login permission from database")
socket.emit("loginRequest", logInUserID!)
print("Test: requested")
socket.on("loginReply") {data, ack in
let jsonLogin = JSON(data)
if jsonLogin[0].intValue == 1{
print("Test: ID Matched, putting up ViewController")
self.prefs.setObject(logInUserID, forKey: "AppUserID")
self.prefs.setInteger(1, forKey: "ISLOGGEDIN")
self.prefs.synchronize()
let permissionToLoadData = self.prefs.integerForKey("ISLOGGEDIN")
if (permissionToLoadData != 1) {
print("Please grant permission to get data")
}else{
print("First view: connecting to database")
importData()
print("Did you import?")
}//permission to load data
self.loginPatientID.resignFirstResponder()
self.dismissViewControllerAnimated(true, completion: nil)
}else if jsonLogin[0].intValue == 0{
self.displayAlertMessage("Sorry, you are not assigned to this program")
}else if jsonLogin[0].intValue == 3{
print("Test: Query problem")
}else{
print("Test: not getting anything from ID database")
}
}//socket.on
}//else
}//login button
override func viewDidLoad() {
super.viewDidLoad()
print("Login View Controller loaded")
}
override func viewDidAppear(animated: Bool) {
socket.connect()
print("LoginVC: establishing connection")
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
You may have noticed that in First view controller, when the viewDidAppear() is launched, the app will checks if user is login or not. If somebody has already logged in, it's fine. If there is nobody logging in, it will perform a segue(modally segue) to Second view controller.
A login form will be presented in second view controller and once user hits the login button, you might wanna look at the code.
Let's assume that everything goes right until it comes to importData(), the function isn't launched at all but the app just goes on, why?
Here's a screenshot of the console, pay attention to "Did you import?", if the function is launched, the app should return some additional message from 1st view controller.
After struggling for a few days, I think I may have found the correct answer.
Eventually I defined 2 different socket handlers connection as such:
let loginSocket = SocketIOClient(socketURL: "localhost:3000")
let socket = SocketIOClient(socketURL: "localhost:3000", options: [
"reconnects": true
])
for both view controller.
If there is a conclusion I can draw from this conundrum is that we can't use single socket handler for socket methods from different view controller.

Resources