How to execute a function from second ViewController after popViewController? - ios

I am an Android developer trying to port my app to iOS and this is my first time working with Swift and Xcode, or anything Mac in general. The app is to scan for a QR code, if the QR code is invalid then I'd like it to go back to the previous screen and display a toast. I've already made a function that displays the toast stating the QR code was invalid.
func found(code: String) {
print(code)
// convert JSON to object
do {
let qrMessage = try JSONDecoder().decode(QrMessage.self, from: code.data(using: .utf8)!)
print("QrMessage object - device name: " + qrMessage.pcName)
}
catch let error {
print(error)
_ = navigationController?.popViewController(animated: true)
// call ShowInvalidQrToast() from the new VC that is now on top of the ViewController stack
}
EDIT:
I managed to figure out a solution using the NotificationCenter from the first answer in this thread: Calling function from another ViewController in swift. More info in my answer below. If you think there is a better way of doing this, please post your own answer.

My solution using NotificationCenter:
In ConnectViewController
override func viewDidLoad() {
super.viewDidLoad()
...
NotificationCenter.default.addObserver(self, selector: #selector(showInvalidQrCodeToast(_:)), name: Notification.Name(rawValue: "showInvalidQrCodeToast"), object: nil)
}
#objc func showInvalidQrCodeToast(_ notification: Notification) {
self.view.makeToast("Invalid QR code")
}
In ScannerViewController
func found(code: String) {
print(code)
// convert JSON to object
do {
let qrMessage = try JSONDecoder().decode(QrMessage.self, from: code.data(using: .utf8)!)
print("QrMessage object - device name: " + qrMessage.pcName)
}
catch let error {
print(error)
_ = navigationController?.popViewController(animated: true)
NotificationCenter.default.post(name: Notification.Name(rawValue: "showInvalidQrCodeToast"), object: nil)
}

Related

iOS - Detecting if an URL stream is working or not, with AVPlayer

This is how in my code, playing from url, looks like:
private func play() {
let streamUrl = ...
let playerItem = AVPlayerItem(url: streamURL)
radioPlayer = AVPlayer(playerItem: playerItem)
radioPlayer.volume = 1.0
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: AVAudioSessionCategoryOptions.mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
UIApplication.shared.beginReceivingRemoteControlEvents()
} catch {
print("Error deactivating audio session.")
}
radioPlayer.play()
startListeningForStreamFail()
stopStartButton.setImage(#imageLiteral(resourceName: "pause_btn"), for: .normal)
}
Like the code snippet explains above, after calling the .play() function, I'm calling startListeningForStreamFail(), which registers the current viewcontroller to two types of notifications, on main thread.
private func startListeningForStreamFail() {
DispatchQueue.main.async { [weak self] in
NotificationCenter.default.addObserver(self as Any, selector: #selector(self?.playerItemFailedToPlay), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: self?.radioPlayer?.currentItem)
NotificationCenter.default.addObserver(self as Any, selector: #selector(self?.playerItemFailedToPlay), name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: self?.radioPlayer?.currentItem)
}
}
And the selector functions is this:
#objc private func playerItemFailedToPlay(notification: Notification) {
print("playerItemFailedToPlay")
}
Because the stream right now works fine, I'm trying to test failure by addig some plus characters in it's url. But the playerItemFailedToPlay() functions does NOT get called, does NOT print anything.
Should this selector getting called, even if only the url was changed?
Any help would be appreciated. Thanks!
I tried to build a project on Github for an easy check
I followed these steps:
Adding NSAppTransportSecurity to info.plist as in this answer to allow http
Trim the provided url to remove any spaces
let urlString = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
Check if the string provides a valid link
guard let url = URL(string: urlString) else { return complete(.unvalidURL) }
Check if the link is playable
AVAsset(url: url).isPlayable
If any of the previous steps was not successful then it means the url is not valid
I also added an observers for the errors after starting a playable link
NotificationCenter.default.addObserver(self, selector: #selector(itemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(itemNewErrorLogEntry(_:)), name: .AVPlayerItemNewErrorLogEntry, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(itemPlaybackStalled(_:)), name: .AVPlayerItemPlaybackStalled, object: nil)
EDIT:
The part of AVAsset(url: url).isPlayable might only check if the path ends with an appropriate extension as an example mp3, mp4.
I do not have access to check the actual code so I would only refer to the documentation

NSNotification getting called multiple times

I have the following code in my didFinishLaunchingWithOptions
NotificationCenter.default.addObserver(
self,
selector: #selector(addressBookDidChange),
name: NSNotification.Name.CNContactStoreDidChange,
object: nil)
This is the method it calls
#objc func addressBookDidChange(notification: NSNotification){
self.processContacts()
}
and here is the notification being removed
func applicationWillTerminate(_ application: UIApplication) {
NotificationCenter.default.removeObserver(NSNotification.Name.CNContactStoreDidChange)
}
The problem is that when I add a new contact via the method below, addressBookDidChange gets called multiple times after, not just once
func addContact(contact:ContactObject) {
let store = CNContactStore()
let contactToAdd = CNMutableContact()
contactToAdd.givenName = contact.firstName
contactToAdd.familyName = contact.lastName
contactToAdd.organizationName = contact.company
for case let contactNumber as PhoneNumberObject in contact.phoneNumbers!{
let mobileNumber = CNPhoneNumber(stringValue: contactNumber.number)
contactToAdd.phoneNumbers.append(CNLabeledValue(label: contactNumber.type.getCNLabelValue(), value: mobileNumber))
}
if let image = contact.image {
contactToAdd.imageData = UIImagePNGRepresentation(image)
}
let saveRequest = CNSaveRequest()
saveRequest.add(contactToAdd, toContainerWithIdentifier: nil)
do {
try store.execute(saveRequest)
} catch {
NSLog("Error adding contact \(contact.firstName) \(contact.lastName) : \(error)")
}
}
How can I make the notification be just called once for an addition of one contact?
I believe that it is not good idea to set notification posting based on delegate and so what should be doing is post notification from delegate checking condition whether change in notification is contact is added.

How to stop GCDAsyncUdpSocket send command?

I am using GCDAsyncUdpSocket for communication between my app and some smart-home hardware, and I have a problem with stopping a certain function. Logic goes something like this:
Send a command
If you didn't receive feedback from the hardware, it'll try to send it a few more times
When app receives feedback, notification DidReceiveDataForRepeatSendingHandler is posted (along with device information in userInfo)
For example, let's say I have a curtain that can react on 3 commands: Open, Close and Stop... and that curtain is currently closed.
I press Open (and don't receive feedback), and during the process I change my mind, so I press Stop. Now the app will send both commands simultaneously.
So without further ado, here's the code:
class RepeatSendingHandler: NSObject {
var byteArray: [UInt8]!
var gateway: Gateway!
var repeatCounter:Int = 1
var device:Device!
var appDel:AppDelegate!
var error:NSError? = nil
var sameDeviceKey: [NSManagedObjectID: NSNumber] = [:]
var didGetResponse:Bool = false
var didGetResponseTimer:Foundation.Timer!
//
// ================== Sending command for changing value of device ====================
//
init(byteArray:[UInt8], gateway: Gateway, device:Device, oldValue:Int) {
super.init()
appDel = UIApplication.shared.delegate as! AppDelegate
self.byteArray = byteArray
self.gateway = gateway
self.device = device
NotificationCenter.default.addObserver(self, selector: #selector(RepeatSendingHandler.didGetResponseNotification(_:)), name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sameDevice(_:)), name: NSNotification.Name(rawValue: NotificationKey.SameDeviceDifferentCommand), object: nil)
sendCommand()
}
func updateRunnableList(deviceID: NSManagedObjectID) {
RunnableList.sharedInstance.removeDeviceFromRunnableList(device: deviceID)
}
// Did get response from gateway
func didGetResponseNotification (_ notification:Notification) {
if let info = (notification as NSNotification).userInfo! as? [String:Device] {
if let deviceInfo = info["deviceDidReceiveSignalFromGateway"] {
if device.objectID == deviceInfo.objectID {
didGetResponse = true
didGetResponseTimer = nil
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
}
}
}
}
func sameDevice(_ notification: Notification) {
print("NOTIFICATION RECEIVED for device with ID: ", self.device.objectID, "\n")
if let info = notification.userInfo as? [NSManagedObjectID: NSNumber] {
sameDeviceKey = info
}
}
func sendCommand () {
if sameDeviceKey != [device.objectID: device.currentValue] { print("keys have DIFFERENT values") } else { print("keys have SAME values") }
if sameDeviceKey != [device.objectID: device.currentValue] {
if !didGetResponse {
if repeatCounter < 4 {
print("Sending command. Repeat counter: ", repeatCounter)
SendingHandler.sendCommand(byteArray: byteArray, gateway: gateway)
didGetResponseTimer = Foundation.Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(RepeatSendingHandler.sendCommand), userInfo: nil, repeats: false)
repeatCounter += 1
} else {
didGetResponseTimer = nil
updateRunnableList(deviceID: device.objectID)
CoreDataController.shahredInstance.saveChanges()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.RefreshDevice), object: self)
}
}else{
didGetResponseTimer = nil
updateRunnableList(deviceID: device.objectID)
CoreDataController.shahredInstance.saveChanges()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationKey.DidReceiveDataForRepeatSendingHandler), object: nil)
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.RefreshDevice), object: self)
}
} else {
print("Command canceled")
didGetResponseTimer = nil
return
}
}
On the ViewController where I keep my devices, I call this like:
func openCurtain(_ gestureRecognizer:UITapGestureRecognizer){
let tag = gestureRecognizer.view!.tag
let address = [UInt8(Int(devices[tag].gateway.addressOne)),UInt8(Int(devices[tag].gateway.addressTwo)),UInt8(Int(devices[tag].address))]
if devices[tag].controlType == ControlType.Curtain {
let setDeviceValue:UInt8 = 0xFF
let deviceCurrentValue = Int(devices[tag].currentValue)
devices[tag].currentValue = 0xFF // We need to set this to 255 because we will always display Channel1 and 2 in devices. Not 3 or 4. And this channel needs to be ON for image to be displayed properly
let deviceGroupId = devices[tag].curtainGroupID.intValue
CoreDataController.shahredInstance.saveChanges()
DispatchQueue.main.async(execute: {
RunnableList.sharedInstance.checkForSameDevice(device: self.devices[tag].objectID, newCommand: NSNumber(value: setDeviceValue))
_ = RepeatSendingHandler(byteArray: OutgoingHandler.setCurtainStatus(address, value: setDeviceValue, groupId: UInt8(deviceGroupId)), gateway: self.devices[tag].gateway, device: self.devices[tag], oldValue: deviceCurrentValue)
})
}
}
What I did was I made a separate class where I have a dictionary that has Device's ManagedObjectID as a key, and the command we are sending is it's value. So whenever we are sending a command for a device that's already on the list, I post a notification SameDeviceDifferentCommand with userInfo containing device's ManagedObjectID and the old command. I use it on RepeatSendingHandler to populate sameDeviceKey dictionary. That's how I tried to distinguish which function should be stopped.
public class RunnableList {
open static let sharedInstance = RunnableList()
var runnableList: [NSManagedObjectID: NSNumber] = [:]
func checkForSameDevice(device: NSManagedObjectID, newCommand: NSNumber) {
if runnableList[device] != nil && runnableList[device] != newCommand {
let oldDataToSend = [device: runnableList[device]!]
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationKey.SameDeviceDifferentCommand), object: self, userInfo: oldDataToSend)
print("Notification sent for device with ID: ", device, "\n")
}
runnableList[device] = newCommand
print("Device with ID: ", device, "received a new command", newCommand, "\n")
}
func removeDeviceFromRunnableList(device: NSManagedObjectID) {
runnableList.removeValue(forKey: device)
print("Removed from list device with ID: ", device)
}
}
However, sometimes it does it's job as it should, and sometimes it doesn't. Using a bunch of prints I tried to see in which order everything happens, and it seems that sometimes even though sameDeviceKey gets it's value from the notification - it looks like it uses old (nil) value until repeatCounter maxes out. I do not understand why.
Could anyone explain what is happening, and/or advise a better solution than the one I provided?
(There is a bit of additional code which I removed as it's irrelevant to logic/question). Please bear in mind that I am a junior and that I'm relatively new to this.

How to send and receive data in Today extensions

I want to develop an app for iOS that have a Widget for notification center, but I don't know how I should send and receive data (pass data) between View Controller and And Today Extension.
I tried to use structs, but it doesn't work, and also I used app groups but I don't want to use this method.
let shared = NSUserDefaults(suiteName: "group.Demo.Share-Extension-Demo.mahdi")
shared?.setObject("Hello", forKey: "kkk")
Apart from NSUserDefaults, you can use the NSNotificationCenter to send or receive data anywhere.
You need to set the observer where you can receive the data like below:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "dataReceived:", name: "SpecialKey", object: nil)
}
Funciton to catch data:
func dataReceived(notification: NSNotification) {
//deal with notification.userInfo
println(notification.userInfo)
println("Received Data")
}
And you need to define NSNotificationCenter from where you need to send the data:
NSNotificationCenter.defaultCenter().postNotificationName("SpecialKey", object: nil, userInfo: ["MyData": "Demo"])
References:
The complete guide to NSNotificationCenter
Hope it helps!
http://moreindirection.blogspot.in/2014/08/nsnotificationcenter-swift-and-blocks.html
For the people who haven't found a way to implement calling function or button click from App Extension (Widget):
Note: This is using Swift
Note 2: Replace the names of NSNotification and methods with your implementations
First, create NotificationCenter post method (In Swift 2.0 - NSNotification Center)
Create methods in App Delegate class -
var scheme: String!
var host: String!
Then, add the following function on the bottom of the class (after the last one):
func application(_ app: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
scheme = url.scheme
host = url.host
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NOTIFICATION_NAME"), object: nil)
return true
}
In your ViewController class, where you want to execute the function or the statement from clicking Widget, add the following in super.viewDidLoad():
NotificationCenter.default.addObserver(self,selector: #selector(self.YOUR_METHOD_NAME),
name: NSNotification.Name(rawValue: "NOTIFICATION_NAME"),
object: nil)
And the method you want to call:
func YOUR_METHOD_NAME(notification: NSNotification) {
let appDelegate =
UIApplication.shared.delegate as! AppDelegate
if appDelegate.scheme != nil {
startRecording()
}
}
I'm assuming that you have already created your widget target, and the view for it. Add this to your button in TodayViewController from which you want to handle the click:
#IBAction func openApp(_ sender: UIButton) {
openApp()
}
And the function to handle opening app by URl Scheme:
func openApp(){
let myAppUrl = NSURL(string: "YOUR_URL_SCHEME://YOUR_HOST_NAME")!
extensionContext?.open(myAppUrl as URL, completionHandler: { (success) in
if (!success) {
self.textView.text = "There was a problem opening app!"
}
})
}
For YOUR_URL_SCHEME, add your scheme that you have specified in Info.plist, if your don't, go to this link and follow instructions:
Add URL Scheme to Xcode
For YOUR_HOST_NAME, you can remove this, and only open app by URL Scheme.
Happy coding!

NSNotification not working on Swift translation (from Objective-C)

After converting my Objective-C code to Swift, I cannot get my NSNotifications to work. After an hour of searching in the web, I finally gave up. Consider the following example:
func getToUrl(url:String, timeoutInterval:Float) -> Bool {
println("Starting HTTP GET to: \(url)")
// Fire a notification
NSNotificationCenter.defaultCenter().postNotificationName("StartNotification", object: self)
[...]
}
func getJsonFromServer() {
// Add an observer which should fire the method test when desired
NSNotificationCenter.defaultCenter().addObserver(self, selector: "test:", name: "StartNotification", object: self)
// Calls the function
getToUrl("http://www.stackoverflow.com", timeoutInterval: 10)
}
func test(sender: AnyObject) {
println("I am here!")
}
I cannot find the error, I would really appreciate if someone else could!
The code runs, but the test method is never called.
Change in this, self to nil (in order to hear all objects)
func getJsonFromServer() {
// Add an observer which should fire the method test when desired
NSNotificationCenter.defaultCenter().addObserver(self, selector: "test:", name: "StartNotification", object: nil)
// Calls the function
getToUrl("http://www.stackoverflow.com", timeoutInterval: 10)
}

Resources