adding URL link to UIButton from JSON data - ios

I am trying to link an button on the UI to a link that I receive after parsing a JSON response. I've added an outlet to the button with the a variable containing the link.
#IBAction func goOnline(sender: AnyObject) {
UIApplication.sharedApplication().openURL(prodURL)
}
I parse the JSON data in a method called in viewDidLoad.
if let convertedJSONIntoDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String: AnyObject] {
if let JSONurl = convertedJSONIntoDict["url"] as? String {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.prodURL = NSURL(string: JSONurl)!
}
}
}
When I click on the button I don't get any response. As always, any help is much appreciated.

i dont have enough reputation to comment so i am adding here
what value do you get in json url???
even you dont need to save the value in the main queue you can also write like
self.prodURL = NSURL(string: JSONurl)!

Swift 3
Define your strings and variables and then it's the following code:
#IBAction func websiteButton(_ sender: Any) {
let websiteURL = ("http://") + JSONurl!
guard let url = URL(string: websiteURL) else {
return //be safe
}
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}
That should work.

Related

Share Extension not working when I long press a link

I've built a sharing extension for my iOS app. The goal is to store all URLs user shared in UserDefaults variable. Here is my code:
import UIKit
import Social
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
override func viewDidLoad() {
let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let propertyList = String(kUTTypePropertyList)
if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
guard let dictionary = item as? NSDictionary else { return }
OperationQueue.main.addOperation {
if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
let urlString = results["URL"] as? String,
let url = NSURL(string: urlString) {
print("URL retrieved: \(urlString)")
let userDefaults = UserDefaults(suiteName: "group.my.app")
if userDefaults?.object(forKey: "extensionArticles") == nil {
userDefaults?.setValue([urlString], forKey: "extensionArticles")
} else {
var urls = userDefaults?.object(forKey: "extensionArticles") as? [String]
urls?.append(urlString)
print("im here in the extension")
dump(urls)
userDefaults?.setValue(urls, forKey: "extensionArticles")
}
}
}
})
} else {
print("error")
}
Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(self.didSelectPost), userInfo: nil, repeats: false)
}
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
print("posted")
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
}
When I go to the webpage in Safari and share it being on that page, everything works fine. However, when I long press (aka 3d touch) on a link in Safari, and use my extension from there, it won't add a link to UserDefaults. I couldn't find how to fix this, plz help 😭

Passing variable outside of a function for use in Swift 3

I'm new to Swift, and I want to 1) run a function that extracts a value from a JSON array (this part works) and 2) pass that variable into another function which will play that URL in my audio player.
My issue: I can't access that string stored in a variable outside the first function. Luckily, there's a bunch of questions on this (example), and they say to establish a global variable outside the function and update it. I have tried this like so:
var audio = ""
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
self.audio = (foo["audio"] as? String)!
} else {
// item could not be found
}
}).resume()
print(audio) // no errors but doesn't return anything
I have confirmed the JSON extraction is working -- if I move that print(audio) inside the function, it returns the value. I just can't use it elsewhere.
I originally tried it without the self. but returned an error.
Is there a better way to store this string in a variable so I can use it in another function?
EDIT: Trying new approach based on Oleg's first answer. This makes sense to me based on how I understand didSet to work, but it's still causing a thread error with the play button elsewhere.
var audiotest = ""{
didSet{
// use audio, start player
if let audioUrl = URL(string: audiotest) {
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationUrl = documentsDirectoryURL.appendingPathComponent(audioUrl.lastPathComponent)
//let url = Bundle.main.url(forResource: destinationUrl, withExtension: "mp3")!
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl)
} catch let error {
print(error.localizedDescription)
}
} // end player
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.com/example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
if let foo = data_list.first(where: {$0["episode"] as? String == "Houston Preview"}) {
// do something with foo
self.audiotest = (foo["audio"] as? String)!
} else {
// item could not be found
}
print(self.audiotest)
}).resume()
The request for the data is asynchronous so the code that is inside the completionHandler block happens some time later (depending on the server or the timeout) , that’s why if you try to print outside the completionHandler actually the print func happens before you get the data.
There are couple of solution:
1. Add property observer to your audio property and start playing when it is set:
var audio = “”{
didSet{
// use audio, start player
}
}
2. Wrapping the request with a method that one of its parameters is a completion closure:
// the request
func fetchAudio(completion:(String)->()){
// make request and call completion with the string inside the completionHandler block i.e. completion(audio)
}
// Usage
fetchAudio{ audioString in
// dispatch to main queue and use audioString
}
Try this code. No need to take global variable if it is not being used in multiple function. you can return fetched URL in completion handler.
func getAudioUrl(completionHandler:#escaping ((_ url:String?) -> Void)) {
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
let audio = (foo["audio"] as? String)!
completionHandler(audio)
} else {
// item could not be found
completionHandler(nil)
}
}).resume()
}
func useAudioURL() {
self.getAudioUrl { (url) in
if let strUrl = url {
// perform your dependant operation
print(strUrl)
}else {
//url is nil
}
}
}

Passing JSON Response From HTTP Request to Another ViewController in Swift 3

I'm new to iOS and was hoping someone would be willing to help me out with an issue I'm having
Say I have 2 Views in my storyboard:
- View1: Has 1 text box
- View2: Has 1 Label
Each being respectively controlled by a ViewControllers:
- FirstViewController
- SecondViewController
My app would send the text of the textbox in View1 as an HTTP (POST) request to an API, and would display on View2 the result which is sent back in JSON format.
My approach is to use the prepare(for segue:,Sender:), however I am having a hard time returning the JSON response from Task() in order to send it to SecondViewController via a Segue.
class ResultViewController: UIViewController {
#IBOutlet var text_input: UITextField!
Let api_url = (the api url)
func makeRequest(voucher_number:String, redemption_code:String){
let json: [String: Any] = [
"input" : text_input.text
]
let request_json = try? JSONSerialization.data(withJSONObject: json)
let url:URL = URL(string: api_url)!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
request.httpBody = request_json
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(
data, response, error) in
guard let _:Data = data, let _:URLResponse = response , error == nil else {
return
}
let json: Any?
do
{
json = try JSONSerialization.jsonObject(with: data!, options: [])
}
catch
{
}
guard let server_response = json as? [String: Any] else
{
return
}
//This is where I think the return should take place
//but can't figure out how
})
task.resume()
}
}
I know I would need to modify my func declaration by adding the return syntax, but I can't figure out how to return data in the first place :P so I skipped this part for the time being.
I would then do the following to send the response to SecondViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "firstSegue" {
if let resultViewController = segue.destination as? SecondViewController {
if (text_input.text != nil && redemption_code.text != nil) {
if let json_response = makeRequest() {
SecondViewController.request_result = json_response
}
// request_result is the variable in
// SecondViewController that will store the data
// being passed via the segue.
}
}
}
}
I know my code may not be the best practice for what I'm trying to achieve. And I'm open to suggestions to tackle a different approach, as long as it's not too advanced for me.
Cheers
Notifications are a good way to forward JSON data out of completion handler blocks, like:
NotificationCenter.default.post(name: Notification.Name(rawValue:"JSON_RESPONSE_RECEIVED"), object: nil, userInfo: server_response)
Register and handle the notification in FirstViewController:
NotificationCenter.default.addObserver(self, selector: #selector(FirstViewController.json_Response_Received(_:)), name:NSNotification.Name(rawValue: "JSON_RESPONSE_RECEIVED"), object: nil)
(in viewDidLoad()) and:
func json_Response_Received(_ notification:Notification) {
responseDictionary = (notification as NSNotification).userInfo as! [String:AnyObject];
self.performSegue(withIdentifier: "SegueToSecondController", sender: self)
}
Then you can pass responseDictionary to SecondViewController in:
override func prepare(for segue:UIStoryboardSegue, sender:Any?) {
if (segue.identifier == "SegueToSecondController") {
secondViewController = segue.destinationViewController as! SecondViewController
secondViewController.response = responseDictionary
}
}

issue passing data to segue.destination within function

I'm trying to make 2 API calls on Segue invoke and ultimately pass Array of Data from Second Call to CollectionView. With first call I'm getting one value catID, which I need in order to make the other call:
let searchEndpoint: String = MY_ENDPOINT
// Add auth key
let serviceCallWithParams = searchEndpoint + "?PARAMETER"
guard let url = URL(string: serviceCallWithParams) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// setting up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// making the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// error check
guard error == nil else {
print("error")
print(error)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse JSON
do {
guard let catData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let data = catData["data"] as? [String: Any] {
if let array = data["categories"] as? [Any] {
if let firstObject = array.first as? [String: Any] {
if let catId = firstObject["catId"] as? Int {
getTitles(catId: catId)
}
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
And then getTitles function looks like this:
func getTitles(catId: Int) {
let catIdString = String(catId)
let titlesEndPoint: String = MY_ENDPOINT + catIdString
// Add auth key
let titlesEndPointWithParams = titlesEndPoint + "?PARAMETER"
guard let titlesUrl = URL(string: titlesEndPointWithParams) else {
print("Error: cannot create URL")
return
}
let titlesUrlRequest = URLRequest(url: titlesUrl)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: titlesUrlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on listCategoryTitles")
print(error)
return
}
// make sure we got data
guard let titlesData = data else {
print("Error: did not receive data")
return
}
// parse the JSON
do {
guard let allTitles = try JSONSerialization.jsonObject(with: titlesData, options: []) as? [String: AnyObject] else {
print("error converting data to JSON")
return
}
if let titlesJson = allTitles["data"] as? [String: Any] {
if let titlesArray = titlesJson["titles"] as? Array<AnyObject> {
self.books = []
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
}
}
} catch {
print("error converting data to JSON")
return
}
}
task.resume()
}
Now when I put:
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
outside function, in target controller I'm getting an empty array as output on first click, but on every next one I'm getting proper data.
When I try putting this inside function "getTitles" the output in CollectionViewController is "nil" every time.
Worth mentioning could be that I have "books" variable defined like so:
Main Controller:
var books: [Book]? = []
Collection Controller:
var books: [Book]?
and I have created type [Book] which is basically object with 3 string variables in separate struct.
All of the code above is encapsulated in
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
Any help/guideline would be much appreciated!
When you make api call it will execute in background means asynchronously where as prepare(for:sender:) will call synchronously.
Now from your question it is looks like that you have create segue in storyboard from Button to ViewController, so before you get response from your api you are moved to your destination controller, to solved your issue you need to create segue from your Source ViewController to Destination ViewController and set its identifier. After that inside getTitles(catId: Int) method after your for loop perform segue on the main thread.
for (index, value) in titlesArray.enumerated() {
var book = Book()
book.bookTitle = value["title"] as? String
book.bookAuthor = value["author"] as? String
if let imageSource = value["_links"] as? Array<AnyObject> {
book.bookImageSource = imageSource[1]["href"] as? String
}
self.books?.append(book)
}
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowResults", sender: nil)
}
After that inside your prepare(for:sender:) make changes like below.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowResults" {
let resultsVC = segue.destination as? CollectionViewController
resultsVC?.books = self.books
}
}

How to get items from extension context in Share extension for iOS?

Once my share extension is loaded, not everytime and not everything is visible for user immediately. The most common is that there you can see image, number of images, and content text. However there are cases where there is a lot more informations.
How to get access to them?
I know that within SLComposeServiceViewController there is extensionContext and its inputItems property.
Ok, so I stopped the debugger at time, and print out on console some things with following command:
po (extensionContext!.inputItems[0] as! NSExtensionItem).userInfo![NSExtensionItemAttachmentsKey]
Is it correct way to do this?
Is there usually one input item?
there was two NSItemProvider objects as attachments to first NSExtensionItem
Ok, then I print out the first of attachments:
How to get that image from that NSItemProvider and url from the next one? Can you deliver some code?
I suppose we will use
loadItemForTypeIdentifier(_:options:completionHandler:)
but do not know how.
import MobileCoreServices
There is a simple function you can apply to your code:
private func fetchAndSetContentFromContext() {
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
return
}
for extensionItem in extensionItems {
if let itemProviders = extensionItem.attachments as? [NSItemProvider] {
for itemProvider in itemProviders {
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
itemProvider.loadItemForTypeIdentifier(kUTTypeText as String, options: nil, completionHandler: { text, error in
})
}
}
}
}
}
So now you know to use the loadItemForTypeIdentifier(_:options:completionHandler:) method to load your aspired data.
In your snapshots you wan to get image and url objects.
Let's begin.
guard
let items = extensionContext?.inputItems as? [NSExtensionItem],
let item = items.first,
let attachments = item.attachments
else { return }
var image: UIImage?
var url: URL?
let semaphore = DispatchSemaphore(value: 2)
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
attachment.loadItem(forTypeIdentifier: kUTTypeImage as String) { item, _ in
image = item as? UIImage
semaphore.signal()
}
}
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
attachment.loadItem(forTypeIdentifier: kUTTypeURL as String) { item, _ in
url = item as? URL
semaphore.signal()
}
}
}
_ = semaphore.wait(timeout: .now() + 1.0)
print(String(describing: image))
print(String(describing: url))

Resources