Integrating CallKit & PushKit to AgoraIO VideoCall - Swift 5 - swift

I can make video calls by using AgoraRtcKit but i cannot send a notification when one device is calling the other.
I have done all the certificates and backgroundmodes things. Here is the viewcontroller's code.
And also i have tried this: Question
I can see the notification when i type the of didReceiveIncomingPushWith in viewDidLoad but cannot see when one device is calling the other. How to make them communicate with each other?
import UIKit
import AgoraRtcKit
import AVFoundation
import CallKit
import PushKit
class VideoCallViewController: UIViewController, CXProviderDelegate, PKPushRegistryDelegate
{
#IBOutlet weak var localView: RoundedCornerView!
#IBOutlet weak var remoteView: UIView!
var apiCaller = ApiCaller()
var videoCall = VideoCall(uId: String(Int.random(in: 1...100000)), channelName: "xxx")
var agoraKit: AgoraRtcEngineKit?
#IBAction func btnEndCall(_ sender: Any)
{
agoraKit?.leaveChannel(nil)
AgoraRtcEngineKit.destroy()
self.dismiss(animated: true, completion: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
initializeAndJoinChannel()
let registry = PKPushRegistry(queue: DispatchQueue.main)
registry.delegate = self
registry.desiredPushTypes = [PKPushType.voIP]
}
func providerDidReset(_ provider: CXProvider) {
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
action.fulfill()
}
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
print(pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined())
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
let config = CXProviderConfiguration()
config.iconTemplateImageData = UIImage(named: "xxx")!.pngData()
config.ringtoneSound = "ringtone.caf"
config.includesCallsInRecents = false;
config.supportsVideo = true;
let provider = CXProvider(configuration: config)
provider.setDelegate(self, queue: DispatchQueue.main)
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: "Person")
update.hasVideo = true
provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
agoraKit?.leaveChannel(nil)
AgoraRtcEngineKit.destroy()
}
#objc func onDidReceiveAlert(_ notification:Notification? = nil) {
print("Received Notification")
}
func initializeAndJoinChannel()
{
apiCaller.videoCallToken(videoCall: videoCall) { [self] rslt in
switch rslt {
case .success(let response):
if response.result.success {
agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: response.appId, delegate: self)
DispatchQueue.main.async { [self] in
agoraKit?.enableVideo()
}
let config = CXProviderConfiguration()
config.iconTemplateImageData = UIImage(named: "xxx")!.pngData()
config.supportsVideo = true;
let provider = CXProvider(configuration: config)
provider.setDelegate(self, queue: DispatchQueue.main)
let controller = CXCallController()
let transaction = CXTransaction(action: CXStartCallAction(call: UUID(), handle: CXHandle(type: .generic, value: "Person")))
controller.request(transaction, completion: { error in })
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 5) {
provider.reportOutgoingCall(with: controller.callObserver.calls[0].uuid, connectedAt: nil)
}
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = UInt(videoCall.uId)!
videoCanvas.renderMode = .hidden
videoCanvas.view = localView
agoraKit?.setupLocalVideo(videoCanvas)
agoraKit?.startPreview()
let option = AgoraRtcChannelMediaOptions()
option.clientRoleType = .broadcaster
agoraKit?.joinChannel(
byToken: response.token, channelId: "xxx", uid: UInt(videoCall.uId)!, mediaOptions: option,
joinSuccess: { (channel, uid, elapsed) in
overlayChange = 1
}
)
}
if response.result.success == false {
self.makeAlert(title: "Hata", message: "Bağlanırken hata oluştu!")
}
case .failure(_):
self.makeAlert(title: "Hata", message: "Veriler alınamadı!")
}
}
}
}
extension VideoCallViewController: AgoraRtcEngineDelegate
{
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int)
{
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.renderMode = .hidden
videoCanvas.view = remoteView
agoraKit?.setupRemoteVideo(videoCanvas)
}
}

Related

Unable to Connect Sign In with Apple to Firebase

When I connect Sign In with Apple to Firebase it comes up with an Error message 'Cannot assign value of type 'LoginPopupViewController' to type 'ASAuthorizationControllerPresentationContextProviding?' It won't show any users as logged in on the Firebase Console.
I followed the second part of this tutorial: https://www.youtube.com/watch?v=BxQsdhglZtE
import Foundation
import UIKit
import AuthenticationServices
import FirebaseAuth
import Firebase
import FirebaseFirestore
import CryptoKit
class LoginPopupViewController: UIViewController, ASAuthorizationControllerDelegate {
#IBAction func doneBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupSignInButton()
}
func setupSignInButton() {
let button = ASAuthorizationAppleIDButton(type: .signIn, style: .white)
button.addTarget(self, action: #selector(handleSignInWithAppleTapped), for: .touchUpInside)
button.frame.size = CGSize(width: 300.0, height: 40.0)
button.center = view.center
view.addSubview(button)
}
#objc func handleSignInWithAppleTapped() {
performSignIn()
}
func performSignIn() {
let request = createAppleIDRequest()
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
func createAppleIDRequest() -> ASAuthorizationOpenIDRequest {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let nonce = randomNonceString()
request.nonce = sha256(nonce)
currentNonce = nonce
return request
}
}
extension ViewController: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let nonce = currentNonce else {
fatalError("Invalid state: A login callback was recieved, but no login request was sent")
}
guard let appleIDToken = appleIDCredential.identityToken else {
print("Unable to fetch identify token")
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
Auth.auth().signIn(with: credential) { (authDataResult, error) in
if let user = authDataResult?.user {
print("Nice! You're now signed in as \(user.uid), email: \(user.email ?? "unknown")")
}
}
}
}
}
extension ViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
}
// Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: Array<Character> =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
// Unhashed nonce.
fileprivate var currentNonce: String?
#available(iOS 13, *)
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()
return hashString
}
You need LoginPopupViewController to conform to ASAuthorizationControllerPresentationContextProviding
extension LoginPopupViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
}
Using FirebaseUI was a lot easier to implement Sign In with Apple and I got the Auth to work and bypassed that Welcome Screen which was pre built:
import Foundation
import UIKit
import AuthenticationServices
import FirebaseAuth
import Firebase
import FirebaseFirestore
import CryptoKit
import FirebaseUI
class LoginPopupViewController: UIViewController, ASAuthorizationControllerDelegate, FUIAuthDelegate {
#IBAction func doneBtn(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupSignInButton()
}
func setupSignInButton() {
let button = ASAuthorizationAppleIDButton(type: .signIn, style: .white)
button.addTarget(self, action: #selector(handleSignInWithAppleTapped), for: .touchUpInside)
button.frame.size = CGSize(width: 300.0, height: 40.0)
button.center = view.center
view.addSubview(button)
}
#objc func handleSignInWithAppleTapped() {
if let authUI = FUIAuth.defaultAuthUI() {
authUI.providers = [FUIOAuth.appleAuthProvider()]
authUI.delegate = self
authUI.signIn(withProviderUI: FUIOAuth.appleAuthProvider(), presenting: self, defaultValue: nil)
// let authViewController = authUI.authViewController()
// self.present(authViewController, animated: true)
}
}
#objc(authUI:didSignInWithAuthDataResult:error:) func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
dismiss(animated: true, completion: nil)
if let user = authDataResult?.user {
print("Nice! You've signed in as \(user.uid). Your email is: \(user.email ?? "") ")
}
}
}

Getting error when using Google place api in swift

I am using google place api to get location in my swift application. In my app when i am going to place api screen by clicking on the textfield present inside the cell of my tableview it is working fine. But when i am using the same code to show the place api screen by clicking on the textfield present inside the uiview, after selecting any place it is comming back to the previous screen, but once again the place api screen opening automatically. i have added my code, can anyonce see it and let me know the solution?
import GooglePlaces
class AddPatientViewController: UIViewController{
//MARK:- Properties
#IBOutlet weak var location_TextField: UITextField!
//MARK:- Variables
var locationManager = CLLocationManager()
var mAppDelegate:AppDelegate = UIApplication.shared.delegate as! AppDelegate;
//MARK:- LifeCycles Methods
override func viewDidLoad() {
super.viewDidLoad()
setDelegate()
}
//MARK:- Helpers
private func setDelegate(){
location_TextField.delegate = self
}
//MARK: - getLocation
func getCurrentLatLongFromCLlocation() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined, .restricted, .denied:
print("No access")
getPermissionAgain()
case .authorizedAlways, .authorizedWhenInUse:
print("Access")
if let coordinateValue = locationManager.location?.coordinate {
mAppDelegate.cordinateVal = coordinateValue
self.getPlaceDataUsingLocation()
}
#unknown default:
getPermissionAgain()
}
} else {
print("Location services are not enabled")
getPermissionAgain()
}
}
func getPermissionAgain(){
let alert = UIAlertController(title: "Simplr", message: "Required location permission to set your search location", preferredStyle: .alert)
let okayAction = UIAlertAction(title: "Dismiss", style: .default, handler: nil)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in
if let appSettings = URL(string: UIApplication.openSettingsURLString) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(appSettings, options: [:], completionHandler: { (enabled) in
DispatchQueue.main.async {
//UIApplication.shared.registerForRemoteNotifications()
}
})
} else {
// Fallback on earlier versions
}
}
}
alert.addAction(okayAction)
alert.addAction(settingsAction)
present(alert, animated: false, completion: nil)
}
func getPlaceDataUsingLocation(){
DispatchQueue.global().async {
// Specify the place data types to return.
let placesClient = GMSPlacesClient.init()
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue) | UInt(GMSPlaceField.coordinate.rawValue) | UInt(GMSPlaceField.formattedAddress.rawValue))
placesClient.currentPlace { (list, error) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = list?.likelihoods {
for likelihood in placeLikelihoodList {
let place = likelihood.place
print("\n\n")
print("Current Place name \(String(describing: place.name)) at likelihood \(likelihood.likelihood)")
print("Current PlaceID \(String(describing: place.placeID))")
print("Formatted Address: \(String(describing: place.formattedAddress))")
// print("Fields: \(place.)")
print("\nAddress Component: \(String(describing: place.addressComponents))\n")
print("Type: \(String(describing: place.types))")
}
}
}
}
}
}
//MARK: - UITextfield Delegate Methods
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool{
if textField == location_TextField{
let autocompleteController = GMSAutocompleteViewController()
autocompleteController.delegate = self
// autocompleteController.primaryTextColor = UIColor(hex: "#4C2B6D")
// autocompleteController.secondaryTextColor = UIColor(hex: "#4C2B6D")
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue) | UInt(GMSPlaceField.coordinate.rawValue) |
UInt(GMSPlaceField.formattedAddress.rawValue) | UInt(GMSPlaceField.addressComponents.rawValue))
autocompleteController.placeFields = fields
autocompleteController.navigationController?.navigationBar.barTintColor = .blue
autocompleteController.navigationController?.navigationBar.prefersLargeTitles = true
// Specify a filter.
let filter = GMSAutocompleteFilter()
filter.type = .geocode
autocompleteController.autocompleteFilter = filter
// Display the autocomplete view controller.
autocompleteController.modalPresentationStyle = .fullScreen
present(autocompleteController, animated: true, completion: nil)
}
return true
}
}
//MARK: - GMSAutocompleteViewControllerDelegate
#available(iOS 13.0, *)
extension AddPatientViewController: GMSAutocompleteViewControllerDelegate {
// Handle the user's selection.
func viewController(_ viewController: GMSAutocompleteViewController, didAutocompleteWith place: GMSPlace) {
print("selected place is \(getFormattedAddressFromAddressComponents(place: place))")
location_TextField.text = getFormattedAddressFromAddressComponents(place: place)
self.dismiss(animated: true)
}
func viewController(_ viewController: GMSAutocompleteViewController, didFailAutocompleteWithError error: Error) {
// TODO: handle the error.
print("Error: ", error.localizedDescription)
}
// User canceled the operation.
func wasCancelled(_ viewController: GMSAutocompleteViewController) {
self.dismiss(animated: true)
}
// Turn the network activity indicator on and off again.
func didRequestAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
func didUpdateAutocompletePredictions(_ viewController: GMSAutocompleteViewController) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
func getFormattedAddressFromAddressComponents(place: GMSPlace) -> String{
var neighborhood = ""
var locality = ""
var sublocality = ""
var administrative_area_level_1 = ""
var formatedAddress = ""
if let addressComponents : Array<GMSAddressComponent> = place.addressComponents{
print(addressComponents)
for component in addressComponents{
if component.types.contains("administrative_area_level_1"){
if let shortName = component.shortName{
administrative_area_level_1 = shortName
}
}
else if component.types.contains("neighborhood")
{
neighborhood = component.name
} else if component.types.contains("sublocality")
{
sublocality = component.name
}else if component.types.contains("locality")
{
locality = component.name
}
}
var shouldAddComma = false
if !isNullObject(anyObject: administrative_area_level_1){
shouldAddComma = true
}
if !isNullObject(anyObject: neighborhood){
formatedAddress = neighborhood
}else if !isNullObject(anyObject: sublocality){
formatedAddress = sublocality
}
else if !isNullObject(anyObject: locality){
formatedAddress = locality
}
if !isNullObject(anyObject: formatedAddress){
if shouldAddComma{
formatedAddress = "\(formatedAddress),\(administrative_area_level_1)"
}
}
}
return formatedAddress
}
}

Empty Roster XMPPFramework Swift

I am building a chat client for iOS using ejabberd and Swift. When I try to retrieve the roster for a user it returns an empty set despite this user having many buddies. Where exactly am I going wrong? I have found similar questions but they seem dated.
Here is my code:
class XMPPController: NSObject {
var hostName: String
var hostPort: UInt16
var password: String
var userJID: XMPPJID
var xmppStream: XMPPStream
var xmppRoster: XMPPRoster!
var xmppRosterStorage: XMPPRosterCoreDataStorage!
init(inputHostName: String, inputUserJIDString: String, inputHostPort: UInt16, inputPassword: String) throws {
guard let formattedUserJID = XMPPJID(string: inputUserJIDString) else {
throw XMPPControllerError.wrongUserJID
}
self.hostName = inputHostName
self.hostPort = inputHostPort
self.password = inputPassword
self.userJID = formattedUserJID
self.xmppStream = XMPPStream()
self.xmppStream.hostName = hostName
self.xmppStream.hostPort = hostPort
self.xmppStream.myJID = userJID
self.xmppRosterStorage = XMPPRosterCoreDataStorage()
self.xmppRoster = XMPPRoster(rosterStorage: xmppRosterStorage)
super.init()
xmppStream.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoster.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoster.autoFetchRoster = true;
xmppRoster.autoAcceptKnownPresenceSubscriptionRequests = true;
xmppRoster.activate(xmppStream)
print(xmppRosterStorage.jids(for: xmppStream))
}
func connect() {
if self.xmppStream.isDisconnected {
}
do {
try self.xmppStream.connect(withTimeout: 5)
} catch {
print("Error Connecting")
}
}
func disconnect(){
self.xmppStream.disconnect()
}
}
extension XMPPController: XMPPStreamDelegate {
func xmppStreamDidConnect(_ sender: XMPPStream) {
print("Stream: Connected")
try! sender.authenticate(withPassword: password)
}
func xmppStreamDidDisconnect(_ sender: XMPPStream, withError error: Error?) {
print("Stream: Disconnected")
}
func xmppStreamDidAuthenticate(_ sender: XMPPStream) {
let presence = XMPPPresence()
self.xmppStream.send(presence)
print("Stream: Authenticated")
NotificationCenter.default.post(name: Notification.Name(rawValue: authenticatedNotificationKey), object: self)
}
func xmppStream(_ sender: XMPPStream, didNotAuthenticate error: DDXMLElement) {
print("Wrong credentials")
}
}
Thank you.
Answering my own questions.
Two problems.
I did not define the superclass XMPPRosterDelegate for XMPPController.
I did not call
func xmppRosterDidEndPopulating(_ sender: XMPPRoster) {
print(xmppRosterStorage.jids(for: xmppStream))
}
something that could only be done having declared XMPPRosterDelegate.
I cannot see to your add any user your roster. You can try this :
func sendSubscribePresenceFromUserRequest ( _ username : String) {
let otherUser = XMPPJID(string: "\(username)#\(xmppDomain)")!
self.roster.addUser(otherUser, withNickname: "Other user name" , groups: nil, subscribeToPresence: true)
}
Then You call setup XMPPRoster method after 'xmppStreamDidAuthenticate' method just like this :
func rosterSetup() {
let storage = XMPPRosterCoreDataStorage.sharedInstance()
roster = XMPPRoster(rosterStorage: storage!, dispatchQueue: DispatchQueue.main)
if let rosterXmpp = roster {
rosterXmpp.setNickname("Your name" , forUser: stream.myJID!)
rosterXmpp.activate(stream)
rosterXmpp.addDelegate(self, delegateQueue: DispatchQueue.main)
rosterXmpp.autoFetchRoster = true
rosterXmpp.autoClearAllUsersAndResources = true
rosterXmpp.autoAcceptKnownPresenceSubscriptionRequests = true
rosterXmpp.fetch()
}
}

Why does my segue not wait until completion handler finished?

I have a page based app, using RootViewController, ModelViewController, DataViewController, and a SearchViewController.
In my searchViewController, I search for an item and then add or remove that Item to an array which is contained in a Manager class(and UserDefaults), which the modelViewController uses to instantiate an instance of DataViewController with the correct information loaded using the dataObject. Depending on whether an Item was added or removed, I use a Bool to determine which segue was used, addCoin or removeCoin, so that the RootViewController(PageView) will show either the last page in the array, (when a page is added) or the first (when removed).
Everything was working fine until I ran into an error which I can not diagnose, the problem is that when I add a page, the app crashes, giving me a "unexpectadely found nil when unwrapping an optional value"
This appears to be the problem function, in the searchViewController 'self.performSegue(withIdentifier: "addCoin"' seems to be called instantly, even without the dispatchque:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
Meaning that In my DataViewController, this function will find nil:
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
I can't find out why it does not wait for completion.
I also get this error about threads:
[Assert] Cannot be called with asCopy = NO on non-main thread.
which is why I try to do the push segue using dispatch que
Here is my searchViewController full code:
import UIKit
class SearchViewController: UIViewController, UISearchBarDelegate {
let selectionLabel = UILabel()
let searchBar = UISearchBar()
let addButton = UIButton()
let removeButton = UIButton()
var filteredObject: [String] = []
var dataObject = ""
var isSearching = false
//Add Button Action.
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
CGPrices.shared.getData(arr: true, completion: { (success) in
print(Manager.shared.coins)
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
})
}
searchBar.text = ""
}
//Remove button action.
#objc func removeButtonActon(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.removeCoin(coin: dataObject)
self.performSegue(withIdentifier: "addCoin", sender: self)
}
searchBar.text = ""
}
//Prepare for segue, pass removeCoinSegue Bool depending on remove or addCoin.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = true
}
} else if segue.identifier == "addCoin" {
if let destinationVC = segue.destination as? RootViewController {
destinationVC.addCoinSegue = false
}
}
}
//Remove button action.
#objc func removeButtonAction(sender: UIButton!) {
if Manager.shared.coins.count == 1 {
removeAlert()
} else {
Manager.shared.removeCoin(coin: dataObject)
print(Manager.shared.coins)
print(dataObject)
searchBar.text = ""
self.removeButton.isHidden = true
DispatchQueue.main.async {
self.performSegue(withIdentifier: "removeCoin", sender: self)
}
}
}
//Search/Filter the struct from CGNames, display both the Symbol and the Name but use the ID as dataObject.
func filterStructForSearchText(searchText: String, scope: String = "All") {
if !searchText.isEmpty {
isSearching = true
filteredObject = CGNames.shared.coinNameData.filter {
// if you need to search key and value and include partial matches
// $0.key.contains(searchText) || $0.value.contains(searchText)
// if you need to search caseInsensitively key and value and include partial matches
$0.name.range(of: searchText, options: .caseInsensitive) != nil || $0.symbol.range(of: searchText, options: .caseInsensitive) != nil
}
.map{ $0.id }
} else {
isSearching = false
print("NoText")
}
}
//Running filter function when text changes.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filterStructForSearchText(searchText: searchText)
if isSearching == true && filteredObject.count > 0 {
addButton.isHidden = false
dataObject = filteredObject[0]
selectionLabel.text = dataObject
if Manager.shared.coins.contains(dataObject) {
removeButton.isHidden = false
addButton.isHidden = true
} else {
removeButton.isHidden = true
addButton.isHidden = false
}
} else {
addButton.isHidden = true
removeButton.isHidden = true
selectionLabel.text = "e.g. btc/bitcoin"
}
}
override func viewDidLoad() {
super.viewDidLoad()
//Setup the UI.
self.view.backgroundColor = .gray
setupView()
}
override func viewDidLayoutSubviews() {
}
//Hide keyboard
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
//Alerts
func removeAlert() {
let alertController = UIAlertController(title: "Can't Remove", message: "\(dataObject) can't be deleted, add another to delete \(dataObject)", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func Duplicate() {
let alertController = UIAlertController(title: "Duplicate", message: "\(dataObject) is already in your pages!", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func max() {
let alertController = UIAlertController(title: "Maximum Reached", message: "\(dataObject) can't be added, you have reached the maximum of 5 coins. Please delete a coin to add another.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
and here is the DataViewController
import UIKit
class DataViewController: UIViewController {
#IBOutlet weak var dataLabel: UILabel!
//Variables and Objects.
//The dataObject carries the chosen cryptocurrencies ID from the CoinGecko API to use to get the correct data to load on each object.
var dataObject = String()
//The DefaultCurrency (gbp, eur...) chosen by the user.
var defaultCurrency = ""
//The Currency Unit taken from the exchange section of the API.
var currencyUnit = CGExchange.shared.exchangeData[0].rates.gbp.unit
var secondaryUnit = CGExchange.shared.exchangeData[0].rates.eur.unit
var tertiaryUnit = CGExchange.shared.exchangeData[0].rates.usd.unit
//Index of the dataObject
var dataIndex = Int()
//Objects
let cryptoLabel = UILabel()
let cryptoIconImage = UIImageView()
let secondaryPriceLabel = UILabel()
let mainPriceLabel = UILabel()
let tertiaryPriceLabel = UILabel()
//Custom Fonts.
let customFont = UIFont(name: "AvenirNext-Heavy", size: UIFont.labelFontSize)
let secondFont = UIFont(name: "AvenirNext-BoldItalic" , size: UIFont.labelFontSize)
//Setup Functions
//Get the index of the dataObject
func getIndex() {
let index = CGPrices.shared.coinData.index(where: { $0.id == dataObject })!
dataIndex = index
}
//Label
func setupLabels() {
//cryptoLabel from dataObject as name.
cryptoLabel.text = CGPrices.shared.coinData[dataIndex].name
//Prices from btc Exchange rate.
let btcPrice = CGPrices.shared.coinData[dataIndex].current_price!
let dcExchangeRate = CGExchange.shared.exchangeData[0].rates.gbp.value
let secondaryExchangeRate = CGExchange.shared.exchangeData[0].rates.eur.value
let tertiaryExchangeRate = CGExchange.shared.exchangeData[0].rates.usd.value
let realPrice = (btcPrice * dcExchangeRate)
let secondaryPrice = (btcPrice * secondaryExchangeRate)
let tertiaryPrice = (btcPrice * tertiaryExchangeRate)
secondaryPriceLabel.text = "\(secondaryUnit)\(String((round(1000 * secondaryPrice) / 1000)))"
mainPriceLabel.text = "\(currencyUnit)\(String((round(1000 * realPrice) /1000)))"
tertiaryPriceLabel.text = "\(tertiaryUnit)\(String((round(1000 * tertiaryPrice) / 1000)))"
}
//Image
func getIcon() {
let chosenImage = CGPrices.shared.coinData[dataIndex].image
let remoteImageUrl = URL(string: chosenImage)
guard let url = remoteImageUrl else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
DispatchQueue.main.async {
self.cryptoIconImage.image = UIImage(data: data)
}
}
}.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
// for family in UIFont.familyNames.sorted() {
// let names = UIFont.fontNames(forFamilyName: family)
// print("Family: \(family) Font names: \(names)")
// }
// Do any additional setup after loading the view, typically from a nib.
self.setupLayout()
self.getIndex()
self.setupLabels()
self.getIcon()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.dataLabel!.text = dataObject
view.backgroundColor = .lightGray
}
}
Edit: CGPrices Class with getData method:
import Foundation
class CGPrices {
struct Coins: Decodable {
let id: String
let name: String
let symbol: String
let image: String
let current_price: Double?
let low_24h: Double?
//let price_change_24h: Double?
}
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i + 1) < coins.count { coinsEncoded += "%2C" }
}
print("encoded")
}
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
guard let url = URL(string: urlJSON) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let coinsData = try JSONDecoder().decode([Coins].self, from: data)
self.coinData = coinsData
completion(arr)
} catch let jsonErr {
print("error serializing json: \(jsonErr)")
print(data)
}
}.resume()
}
func refresh(completion: () -> ()) {
defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
completion()
}
}
I figured it out.
The problem was inside my getData method I was not updated the coins array:
var coinData = [Coins]()
var defaultCurrency = ""
var coins = Manager.shared.coins
var coinsEncoded = ""
static let shared = CGPrices()
func encode() {
for i in 0..<coins.count {
coinsEncoded += coins[i]
if (i+1)<coins.count { coinsEncoded+="%2C" }
}
print("encoded")
}
I needed to add this line in getData:
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
//Adding this line to update the array so that the URL is appended correctly.
coins = Manager.shared.coins
encode()
let urlJSON = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=\(coinsEncoded)"
This would fix the finding nil in the DataViewController, but the app would still crash do to updating UI Elements on a background thread, as the segue was called inside the completion handler of the getData method. to fix this, I used DispatchQue.Main.Async on the segue inside the getData method in the addButton function, to ensure that everything is updated on the main thread, like so:
#objc func addButtonAction(sender: UIButton!) {
print("Button tapped")
if Manager.shared.coins.contains(dataObject) {
Duplicate()
} else if Manager.shared.coins.count == 5 {
max()
} else {
Manager.shared.addCoin(coin: dataObject)
print("starting")
CGPrices.shared.getData(arr: true) { (arr) in
print("complete")
print(CGPrices.shared.coinData)
//Here making sure it is updated on main thread.
DispatchQueue.main.async {
self.performSegue(withIdentifier: "addCoin", sender: self)
}
}
}
searchBar.text = ""
}
Thanks for all the comments as they helped me to figure this out, and I learned a lot in doing so. Hopefully this can help someone else in their thought process when debugging, as one can get so caught up in one area of a problem, and forget to take a step back and look to other areas.

Swift Drag and Drop Mail on OS X 10.13

I have had dragging and dropping working for Mail working for a while now. That was until I upgraded to OSX 10.13.
Here is my code:
class DropView: NSView
{
var filePath: String?
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.red.cgColor
registerForDraggedTypes([NSPasteboard.PasteboardType
.fileNameType(forPathExtension: ".eml"), NSPasteboard.PasteboardType.filePromise])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
if true
{
self.layer?.backgroundColor = NSColor.blue.cgColor
return .copy
}
}
override func draggingExited(_ sender: NSDraggingInfo?)
{
self.layer?.backgroundColor = NSColor.red.cgColor
}
override func draggingEnded(_ sender: NSDraggingInfo)
{
self.layer?.backgroundColor = NSColor.gray.cgColor
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
let pasteboard: NSPasteboard = sender.draggingPasteboard()
let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver]
let folderPath = NSHomeDirectory()+"/Drop Stuff/"
if (!FileManager.default.fileExists(atPath: folderPath))
{
do
{
try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil)
}
catch
{
print ("error")
}
}
let folderURL = NSURL(fileURLWithPath: folderPath)
let f = sender.namesOfPromisedFilesDropped(atDestination: folderURL as URL)
print (f!)
print ("Copied to \(folderPath)")
return true
}
}
The problem is that namesOfPromisedFilesDropped is returning the name of the parent folder not the name of the file as it did on previous version of the OS.
The compiler warns that namesOfPromisedFilesDropped is deprecated. Good job Apple for not providing any documentation on the new stuff. Thanks to StackOverflow I managed to piece this together which works using the new APIs, but still exhibits the same problem as above.
class DropView2: NSView
{
var filePath: String?
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.red.cgColor
registerForDraggedTypes([NSPasteboard.PasteboardType
.fileNameType(forPathExtension: ".eml"), NSPasteboard.PasteboardType.filePromise])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
if true
{
self.layer?.backgroundColor = NSColor.blue.cgColor
return .copy
}
}
override func draggingExited(_ sender: NSDraggingInfo?)
{
self.layer?.backgroundColor = NSColor.red.cgColor
}
override func draggingEnded(_ sender: NSDraggingInfo)
{
self.layer?.backgroundColor = NSColor.gray.cgColor
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
let pasteboard: NSPasteboard = sender.draggingPasteboard()
guard let filePromises = pasteboard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver] else {
return false
}
print ("Files dropped")
var files = [URL]()
let filePromiseGroup = DispatchGroup()
let operationQueue = OperationQueue()
let destURL = URL(fileURLWithPath: "/Users/andrew/Temporary", isDirectory: true)
print ("Destination URL: \(destURL)")
filePromises.forEach ({ filePromiseReceiver in
print (filePromiseReceiver)
filePromiseGroup.enter()
filePromiseReceiver.receivePromisedFiles(atDestination: destURL,
options: [:],
operationQueue: operationQueue,
reader:
{ (url, error) in
print ("Received URL: \(url)")
if let error = error
{
print ("Error: \(error)")
}
else
{
files.append(url)
}
print (filePromiseReceiver.fileNames, filePromiseReceiver.fileTypes)
filePromiseGroup.leave()
})
})
filePromiseGroup.notify(queue: DispatchQueue.main,
execute:
{
print ("Files: \(files)")
print ("Done")
})
return true
}
}
I'm using 10.13.2. Am I doing something wrong or is this a bug?
It's driving me nuts.