iOS swift 4 avplayer for YouTube External Link - swift4

I wonder why it cannot play the External link by "avplayer"?
Only pops out an empty AVplayerViewController but plays nothing.
import UIKit
import AVFoundation
import AVKit
class DetailTableViewController: UITableViewController {
var article: Article!
var player:AVPlayer?
var playerItem:AVPlayerItem?
.......
..........
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func playUsingAVPlayer(url: URL) {
player = AVPlayer(url: url)
player?.play()
}
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath)
cell.playVideo?.addControlEvent(.touchUpInside) {
let videoURL = URL(string: "http://www.youtube.com/playlist?list=PLWYak5Af5-DvboTzxQYeg7aKYA9UHYwSf")
let player = AVPlayer(url: videoURL!)
let avpvv = AVPlayerViewController()
avpvv.player = player
self.present(avpvv, animated: true){
avpvv.player!.play()
}
}
cell.playSoundButton.addControlEvent(.touchUpInside, {
self.tableView.reloadData()
guard let url = URL(string: "http://mediasys.taipei.gov.tw/tcg/service/KMStorage/355/894E598B-8A9F-BAA8-206D-8DF52A8C5763/Panda_Voice01.mp3" )
else {
return
}
if let myplayer = self.player{
if ((myplayer.rate != 0) && (myplayer.error == nil)) {
myplayer.pause()
playUsingAVPlayer(url:url )
}
}else{
playUsingAVPlayer(url:url )
}
})
}
else { print ("error to get cell back") }
return cell
}
........
}
The ATS is set for all loads. If you have any idea why this is not working, please let me know.
Thanks in advance.

AVPlayer isn’t capable of playing the YouTube link, which is why it fails to load. YouTube links are actually web pages with an embedded player, and not something that is readable by AVKit (which expects a media file or HLS stream). As the other answer pointed out, if you want to show YouTube content in a somewhat-native like way, you will need to use Google’s library (which actually just renders it in a web view). https://developers.google.com/youtube/v3/guides/ios_youtube_helper Will provide information on how to use the library - but its not possible to use the stock AVKit classes with YouTube link.

Related

How to properly display parsed data in table view after receiving them through completion handler

I need to show info about movies(taken from https://developers.themoviedb.org/) in tableView. I'm doing network request using a singleton and then pass parsed data to tableViewController through completion handler. I can print received data but I can't properly set them in tableView cell. Could you please help me how to fix this problem.
Network Manager
func getMovies(completion: #escaping ([Movies]?) -> Void) {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/now_playing?api_key=\(apiKey)&language=en")
else { fatalError("Wrong URL") }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let jsonData = data {
let decoder = JSONDecoder()
do {
let moviesResult = try decoder.decode(MoviesResult.self, from: jsonData)
let movies = moviesResult.results
completion(movies)
}
catch {
print(error)
}
}
}.resume()
}
Movies View Controller
var movies = [Movies]()
override func viewDidLoad() {
super.viewDidLoad()
network.getMovies { result in
if let result = result {
self.movies = result
print(self.movies)
}
}
extension MoviesViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let movie = movies[indexPath.row]
print(movie)
if let cell = tableView.dequeueReusableCell(withIdentifier: "moviesMainInfo", for: indexPath) as? MovieTableViewCell {
cell.filmTitle.text = movie.title
cell.filmRating.text = String(movie.popularity!)
return cell
}
return UITableViewCell()
}
}
Parsed result: [MovieApp.Movies(genreIDs: Optional([14, 28, 12]), overview: Optional("Wonder Woman comes into conflict with the Soviet Union during the Cold War in the 1980s and finds a formidable foe by the name of the Cheetah."), popularity: Optional(1927.057), title: Optional("Wonder Woman 1984"), releaseDate: Optional("2020-12-16"), posterPath: Optional("/8UlWHLMpgZm9bx6QYh0NFoq67TZ.jpg")),
You are doing everything correctly, you just need to reload your UITableView when data arrives. Be aware that you need to reload your UITableView on the main thread, because UIKit isn't thread safe:
otherwise your application will most probably crash:
private func reloadTableView() {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Also I encourage you to extract your networking function from viewDidLoad. An other improvement is to use [weak self] in your closures to avoid memory leaks:
private func loadData() {
network.getMovies { [weak self] result in
if let result = result {
self?.movies = result
print(self?.movies)
self?.reloadTableView()
} else {
// Maybe show some info that the data could not be fetched
}
}
}
And in your viewDidLoad just call it:
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}

swift: index out of range

I want to play some songs from a folder that I have added to xcode.
I use tabbed application and the code is like this:
func playThis(thisOne:String)
{
do
{
let audioPath = Bundle.main.path(forResource: thisOne, ofType: ".mp3")
try audioPlayer = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
audioPlayer.play()
}
catch
{
print ("ERROR")
}
}
override func viewDidLoad() {
super.viewDidLoad()
label.text = songs[thisSong] //error index out of range
}
But when I run it, the song not appear in first
view controller and when I click the second view controller tab app crashes and the reason is:
index out of range
First view controller code is like this:
import UIKit
import AVFoundation
var audioPlayer = AVAudioPlayer()
var songs:[String] = []
var thisSong = 0
var audioStuffed = false
class FirstViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var myTableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return songs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
cell.textLabel?.text = songs[indexPath.row]
return cell
}
and my way to pass the song to detail view controller:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
do
{
let audioPath = Bundle.main.path(forResource: songs[indexPath.row], ofType: ".mp3")
try audioPlayer = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
audioPlayer.play()
thisSong = indexPath.row
audioStuffed = true
}
catch
{
print ("ERROR")
}
}
second view controller code:
override func viewDidLoad()
{
super.viewDidLoad()
gettingSongNames()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
//FUNCTION THAT GETS THE NAME OF THE SONGS
func gettingSongNames()
{
let folderURL = URL(fileURLWithPath:Bundle.main.resourcePath!)
do
{
let songPath = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
//loop through the found urls
for song in songPath
{
var mySong = song.absoluteString
if mySong.contains(".mp3")
{
let findString = mySong.components(separatedBy: "/")
mySong = findString[findString.count-1]
mySong = mySong.replacingOccurrences(of: "%20", with: " ")
mySong = mySong.replacingOccurrences(of: ".mp3", with: "")
songs.append(mySong)
}
}
myTableView.reloadData()
}
catch
{
print ("ERROR")
}
}
Not getting enough conditions from the provided code but if you want to stop the crash handle the exceptions and handle every scenario of the code
Example: always check the array.count > 0 before accessing the values.
In your case :
if songs.count > 0 {
label.text = songs[thisSong]
}

images can't be display in table view

I created a tableView with custom cells that each cell has an image.
In the model class, I created a func mainPulatesData() to use URLSession dataTask method to retrieve data from url, and convert data into UIImage in the completion handler block, then add image into an variable of array of UIImage.
The process of retrieve data and adding them into UIImage array was perform in DispatchQueue.global(qos: .userInitiated).async block. based on the print message, the images did be added into array.
however, even I created an instance of model class in tableView controller, and invokes the mainPulatesData() in viewDidlLoad, the image didn't show up in the table.
Based on other print message in table view controller class, I found even it can be added into array in model class, but it seems like doesn't work on the instance of model class in tableView controller.
that's the code in model class to gain image data:
func mainPulatesData() {
let session = URLSession.shared
if myURLs.count > 0{
print("\(myURLs.count) urls")
for url in myURLs{
let task = session.dataTask(with: url, completionHandler: { (data,response, error) in
let imageData = data
DispatchQueue.global(qos: .userInitiated).async {
if imageData != nil{
if let image = UIImage(data: imageData!){
self.imageList.append(image)
print("\(self.imageList.count) images added.")
}
}
else{
print("nil")
}
}
})
task.resume()
}
}
}
that's the code in view controller to create instance of model:
override func viewDidLoad() {
super.viewDidLoad()
myModel.mainPulatesURLs()
myModel.mainPulatesData()
loadImages()
}
private func loadImages(){
if myModel.imageList.count > 0{
tableView.reloadData()
}
else{
print("data nil")
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myModel.imageList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
if myModel.imageList.count > 0{
let image = myModel.imageList[indexPath.row]
cell.tableImage = image
return cell
}
return cell
}
The reason is that the images or imageList isn't ready by the time cellForRowAt is called after you reloadData().
A good practice is to use placeholder images in the beginning and only load image when a table view cell is visible instead of everything at once. Something like:
// VC class
private var modelList = [MyModel(url: url1), MyModel(url: url2), ...]
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return modelList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as! ImageTableViewCell
cell.update(model: modelList[indexPath.row])
return cell
}
// Cell class
#IBOutlet weak var cellImageView: UIImageView!
func update(model: MyModel) {
model.fetchImage(callback: { image in
self.cellImageView.image = image
})
}
// Model class
final class MyModel: NSObject {
let url: URL
private var _imageCache: UIImage?
init(url: URL) {
self.url = url
}
func fetchImage(callback: #escaping (UIImage?) -> Void) {
if let img = self._imageCache {
callback(img)
return
}
let task = URLSession.shared.dataTask(with: self.url, completionHandler: { data, _, _ in
if let imageData = data, let img = UIImage(data: imageData) {
self._imageCache = img
callback(img)
} else {
callback(nil)
}
})
task.resume()
}
}
Images are not displayed because you download them in background thread (asynchronously), and loadImages() is called synchronously. That means loadImage() is called before myModel.mainPulatesData() is executed, so when your images are downloaded, tableview is not being updated (reloadData() is not called). You should create Protocol to notify UIViewController, that data has been downloaded, or use Completion Handler.
Simple example of handler I am using, I call this in my viewDidLoad, it requests data from server and return an array of [Reservation]?
ReservationTableModule.requestAllReservations { [weak self] reservations in
guard let `self` = self else {
return
}
guard let `reservations` = `reservations` else {
return
}
self.reservations = reservations
.reservationsTableView.reloadData()
}
this is actual request function
class func requestAllReservations(handler: #escaping ([Reservation]?) -> Void) {
let url = "reservations/all"
APIModel.shared.requestWithLocation(.post, URL: url, parameters: nil) { data in
let reservations = data?["reservations"].to(type: Reservation.self) as? [Reservation]
handler(reservations)
}
}
handler: #escaping ([Reservation]?) -> Void is called completion handler, you should, I guess make it handler: #escaping ([UIImage]?) -> Void and after your data downloaded call handler(reservations)
You should take note of the sequence of your function call here:
myModel.mainPulatesURLs() --> populates myURLs
myModel.mainPulatesData() --> populates image from myURLs in forloop asynchronously.
loadImages() --> called asynchronously.
while you're loading your images from myModel.mainPulatesData() you already called loadImages() which myModel.imageList was still empty.
you should call loadImages() after a callback from myModel.mainPulatesData() or when you're sure that the images where already loaded.
you can use dispatch_group_t to configure the callbacks.
here as requested:
import UIKit
var myURLs: [String] = ["urlA", "urlB", "urlC", "urlD"]
// we define the group for our asynchronous fetch. also works in synchronous config
var fetchGroup = DispatchGroup()
for urlString in myURLs {
// for every url fetch we define, we will call an 'enter' to issue that there is a new block for us to wait or monitor
fetchGroup.enter()
// the fetch goes here
let url = URL(string: urlString)!
URLSession.shared.downloadTask(with: URLRequest(url: url), completionHandler: { (urlReq, urlRes, error) in
// do your download config here...
// now that the block has downloaded the image, we are to notify that it is done by calling 'leave'
fetchGroup.leave()
}).resume()
}
// now this is where our config will be used.
fetchGroup.notify(queue: DispatchQueue.main) {
// reload your table here as all of the image were fetched regardless of download error.
}

NSOperation for loading image to TableView

Here is my Movie class:
import UIKit
class Movie {
var title: String = ""
var rating: Double = 0
var image: UIImage = UIImage()
}
I want to load the the array of movie to tableView, I have tried like this:
import UIKit
import Cosmos
import Alamofire
//import AlamofireImage
import SwiftyJSON
class Downloader {
class func downloadImageWithURL(url:String) -> UIImage! {
let data = NSData(contentsOfURL: NSURL(string: url)!)
return UIImage(data: data!)
}
}
class MovieViewController: UIViewController, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var movies: [Movie] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchMovies()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! MovieTableViewCell
let movie = movies[indexPath.row]
cell.movieTitleLabel.text = movie.title
cell.movieRatingView.rating = Double(movie.rating / 20)
cell.movieImageView.image = movie.image
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return movies.count
}
func fetchMovies() {
let movieURLString = "https://coderschool-movies.herokuapp.com/movies?api_key=xja087zcvxljadsflh214"
Alamofire.request(.GET, movieURLString).responseJSON { response in
let json = JSON(response.result.value!)
let movies = json["movies"].arrayValue
let queue = NSOperationQueue()
for movie in movies {
let title = movie["title"].string
let rating = movie["ratings"]["audience_score"].double
let imageURLString = movie["posters"]["thumbnail"].string
let movie = Movie()
movie.title = title!
movie.rating = rating!
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
self.movies.append(movie)
self.tableView.reloadData()
}
}
}
}
The problem is that: when I scroll down or up, the images will be reload, otherwise they are not reloaded. Only the title and rating will be
I only know the reason is these line
self.movies.append(movie)
self.tableView.reloadData()
are compiled before
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
})
queue.addOperation(operation)
But if I scroll down, it will like this:
I've already do it perfectly with AlamofireImage, but I really want to use NSOperation for diving with it.
I think you should make sure to reloadData() on the main thread. By default it's happening on a background thread and cannot update the UI.
dispatch_async(dispatch_get_main_queue(),{
self.tableView.reloadData()
})
I solved the problem by put the reloadData to the main thread, like so:
let operation = NSBlockOperation(block: {
movie.image = Downloader.downloadImageWithURL(imageURLString!)
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.tableView.reloadData()
}
})
Now it works well.

Displaying PDF from list in WebView using swift

I have a list of PDF's embedded in my app. I would like to use webView to display one pdf at a time, selected from a table view controller. The example program, from Neil Smith, uses the webView to display web pages using the following code snippet from a detail view controller:
if let address = webSite {
let webURL = NSURL(string: address)
let urlRequest = NSURLRequest(URL: webURL!)
webView.loadRequest(urlRequest)
The following code snippet does what I want it to do, but only for the file "TestPDF"
if let pdf = NSBundle.mainBundle().URLForResource("TestPDF", withExtension: "pdf",
subdirectory: nil, localization: nil) {
let req = NSURLRequest(URL: pdf)
let webView = UIWebView(frame: CGRectMake(20,20,self.view.frame.size.width-40,self.view.frame.size.height-40))
webView.loadRequest(req)
self.view.addSubview(webView)
I would like to change this so that it selects a PDF from a list, for example:
pdfAddresses = [
"TestPDF.pdf",
"TestPDF2.pdf",
"TestPDF3.pdf",
"TestPDF4.pdf",
"testPDF5.pdf"]
The segue code works on the webAddress list just fine:
if segue.identifier == "ShowAttractionDetails" {
let detailViewController = segue.destinationViewController
as! AttractionDetailViewController
let myIndexPath = self.tableView.indexPathForSelectedRow()
let row = myIndexPath?.row
detailViewController.webSite = webAddresses[row!]
So what I don't know what to do is how to select the pdf file, based on the table view controller. I think I need to create a variable... but I'm not sure. Thank you for you suggestions.
Here is simple example for you:
TableViewController.swift
import UIKit
class TableViewController: UITableViewController {
//Your PDF list
var pdfAddresses = [
"TestPDF",
"TestPDF2",
"TestPDF3",
"TestPDF4"]
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pdfAddresses.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
// Configure the cell...
cell.textLabel?.text = pdfAddresses[indexPath.row] //Display PDF list in tableView
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowAttractionDetails" {
let detailViewController = segue.destinationViewController as! AttractionDetailViewController
let myIndexPath = self.tableView.indexPathForSelectedRow()
let row = myIndexPath?.row
//Pass selected cell title to next View
detailViewController.webSite = pdfAddresses[row!]
}
}
}
AttractionDetailViewController.swift
import UIKit
class AttractionDetailViewController: UIViewController {
//Outlet of webView but you can create it programatically too.
#IBOutlet weak var webView: UIWebView!
//This will hold data which is passed from first View
var webSite : String?
override func viewDidLoad() {
super.viewDidLoad()
//Load selected PDF into webView.
if let webSite = webSite {
println(webSite)
if let pdf = NSBundle.mainBundle().URLForResource(webSite, withExtension: "pdf", subdirectory: nil, localization: nil) {
let req = NSURLRequest(URL: pdf)
webView.loadRequest(req)
}
}
}
}
Check this SAMPLE project for more Info.