How to play sound from one button based off of UIimageview image - swift

i've developed a fully working flash card app for kids. It has a UIImageView that cycles thru 26 cards (abcs) via a gesture click and a music button that will play a sound for each image. Right now i have this 100% working BUT the sound plays in an IF statement that has added an additional 400 lines of code.
An example of the music button:
if (imageView.image?.isEqual(UIImage(named: "card1")))! {
do { audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: aSound))
audioPlayer.play()
} catch {
print("Couldn't load sound file")
}
}
else if (imageView.image?.isEqual(UIImage(named: "card2")))! {
do { audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: bSound))
audioPlayer.play()
} catch {
print("Couldn't load sound file")
}
}
else if (imageView.image?.isEqual(UIImage(named: "card3")))! {
do { audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: cSound))
audioPlayer.play()
} catch {
print("Couldn't load sound file")
}
}
I know i could set a sound array with the sound files in it, but how would i tie it back to the card? I cant set a tag property on an asset image can i?
Looking for a way to shorten the current code i have.

here is a way I would go about. Haven't tested it but it should get you there.! I use arrays, but Set or Dictionary would work.
1) Create a struct
struct Card {
let soundURL: URL
let image: UIImage
}
2) gather all the cards in an Array:
let cards: [Card] = {
var collection = [Card]()
let soundPaths: [String] = ["aSound", "bSound", "xSound", "zSound"]
let cardNames: [String] = ["card1", "card2", "card25", "card26"]
// array of all 26 urls
var soundUrls: [URL] {
var urls = [URL]()
for path in soundPaths {
urls.append(URL(fileURLWithPath: path))
}
return urls
}
// array of all 26 images
var cardImages: [UIImage] {
var images = [UIImage]()
for name in cardNames {
guard let image = UIImage(contentsOfFile: name) else { continue }
images.append(image)
}
return images
}
// not the best approach but if you have sound and naming
// in the order then:
for idx in 0..<26 {
let card = Card(soundURL: soundUrls[idx], image: cardImages[idx])
collection.append(card)
}
return collection
}()
3) add an extension to UIImage to play the sound:
extension UIImage {
func playSound() {
let card = cards.filter { $0.image == self }.first
if card != nil {
do {
let audioPlayer = try AVAudioPlayer(contentsOf: card!.soundURL)
audioPlayer.play()
} catch {
print("Couldn't load the sound file with error: \(error)")
}
}
}
}
Then when you get your imageView.image, you should be able to just do:
imageView.image?.playSound()
Again I didn't test this, but I think the logic still stand. Hope it can help.

This approach is completely wrong. Your image view is a view. You should not make any choices based on what it displays. You should make your choice based on something about your data model.
For example, let's say you have three images, and an array of three image names:
let images = ["Manny", "Moe", "Jack"]
var currentImageIndex = 0
And let's say you change the image index and display that image:
self.currentImageIndex = 1
self.imageView.image = UIImage(named:images[currentImageIndex])
Then the way you know which image is displayed is by looking at currentImageIndex — not by looking at the image view.
In real life, your model may be much more extensive, for example a struct and an array of structs. But the principle remains exactly the same. It is the model that tells you what to do, never the interface.

Related

How to set NowPlaying properties with a AVQueuePlayer in Swift?

I have an AVQueuePlayer that gets songs from a Firebase Storage via their URL and plays them in sequence.
static func playQueue() {
for song in songs {
guard let url = song.url else { return }
lofiSongs.append(AVPlayerItem(url: url))
}
if queuePlayer == nil {
queuePlayer = AVQueuePlayer(items: lofiSongs)
} else {
queuePlayer?.removeAllItems()
lofiSongs.forEach { queuePlayer?.insert($0, after: nil) }
}
queuePlayer?.seek(to: .zero) // In case we added items back in
queuePlayer?.play()
}
And this works great.
I can also make the lock screen controls appear and use the play pause button like this:
private static func setRemoteControlActions() {
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [self] event in
queuePlayer?.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [self] event in
if queuePlayer?.rate == 1.0 {
queuePlayer?.pause()
return .success
}
return .commandFailed
}
}
The problem comes with setting the metadata of the player (name, image, etc).
I know it can be done once by setting MPMediaItemPropertyTitle and MPMediaItemArtwork, but how would I change it when the next track loads?
I'm not sure if my approach works for AVQueueplayer, but for playing live streams with AVPlayer you can "listen" to metadata receiving.
extension ViewController: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
//look for metadata in groups
}
}
I added the AVPlayerItemMetadataOutputPushDelegate via an extension to my ViewController.
I also found this post.
I hope this gives you a lead to a solution. As said I'm not sure how this works with AVQueuePlayer.

How can I use firebase storage to download images in a file and show them in a table view?

Good afternoon,
I have been stuck on this problem for months. I am trying to use firebase storage to save image files that a user uploaded. The program should then be able to update the queue and show the image in a horizontal table view. Kinda like netflix where its titles of movies/shows but mine would just be pictures. After trying to figure this out, this is what I came up with. Here is to receive the images
class ImageRecieve : ObservableObject {
#Published var songImageArrayURL = [URL]()
#Published var data : Data?
#Published var songImage : NSImage?
#Published var AlbumCoverArray = [NSImage]()
func GetURLS(){
//we want to get the download urls
bfRef.listAll { (result, error) in
if let error = error{ //if theres an error, print it
print(error.localizedDescription)
}
let prefixes = result.prefixes
//loop to search each song prefix
for i in prefixes.indices{
//get the song of each prefix
prefixes[i].listAll { (result, error) in
if let error = error {
print(error.localizedDescription)
}
else {
let items = result.items
//if anything contains ".mp3" dont add it to array.
for j in items.indices{
if(!items[j].name.contains("mp3")){
SongImage.append(items[j])
self.download(SongImage: items[j])
}
}
}
}
}
}
}
func download(SongImage:StorageReference){
//get download url
DispatchQueue.main.async {
SongImage.downloadURL { (url, error) in
if let error = error { //if there is an error print it
print(error.localizedDescription)
}
else {
if(url != nil){
self.songImage = NSImage(byReferencing: url!)
self.AlbumCoverArray.append(self.songImage!)
}
}
}
}
}
func load(){
if(self.songImageArrayURL.isEmpty){
GetURLS()
}
print(self.songImageArrayURL)
for i in self.songImageArrayURL.indices{
print(self.songImageArrayURL[i])
DispatchQueue.global().async{
if let data = try? Data(contentsOf: self.songImageArrayURL[i]){
if let image = NSImage(data:data){
DispatchQueue.main.async {
self.songImage = image
}
}
}
}
}
}
func cancel(){
}
}
here is to load the images :
struct LoadImages<Placeholder: View>: View {
#ObservedObject var loader : ImageRecieve
private var placeholder : Placeholder?
init(placeholder: Placeholder? = nil) {
loader = ImageRecieve()
self.placeholder = placeholder
}
var body: some View {
image
.onAppear(perform: loader.GetURLS)
.onDisappear(perform: loader.cancel)
}
private var image: some View{
ForEach(loader.AlbumCoverArray.indices,id:\.self){
i in
Group{
if(self.loader.songImage != nil){
Image(nsImage:self.loader.AlbumCoverArray[i]).resizable().frame(width:50, height:50)
}
else{
self.placeholder
}
}
}
}
}
the problem I've been stuck on is that the photos are only downloading one at a time and not listing one by one. For example, they show one image and then switch to the next. I would like an array of images. So that the images get added to the list. I've tried using an image array but it doesnt work.
photos are only downloading one at a time and not listing one by one.
in all languages an array/list is processed sequentially, you might want to use multi-Threading for parallelism. use a queue and assign few threads which download image, after each download pop the element from queue.
all the child threads append/push the data to the main thread. in that manner you will be able to display images as they load.
PS:i am != swiftie but seeing your programming i sense turmoil. try improving your code grammar and avoid too many functions and spaces.

Convert images to videos in iOS without locking UI Thread (Swift)

I am trying to convert image snapshots into a video but I am facing UI Thread problems: my view controller is locked. I would like to know how to handle this because I did a lot of research and tried to detach the process into different DispatchQueues but none of them worked. So, it explains why I am not using any Queue on the code below:
class ScreenRecorder {
func renderPhotosAsVideo(callback: #escaping(_ success: Bool, _ url: URL)->()) {
var frames = [UIImage]()
for _ in 0..<100 {
let image = self.containerView.takeScreenshot()
if let imageData = UIImageJPEGRepresentation(image, 0.7), let compressedImage = UIImage(data: imageData) {
frames.append(compressedImage)
}
}
self.generateVideoUrl(frames: frames, complete: { (fileURL: URL) in
self.saveVideo(url: fileURL, complete: { saved in
print("animation video save complete")
callback(saved, fileURL)
})
})
}
}
extension UIView {
func takeScreenshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let image = renderer.image { _ in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
class ViewController {
let recorder = ScreenRecorder()
recorder.renderPhotoAsVideo { success, url in
if (success) {
print("ok")
} else {
self.alert(title: "Erro", message: "Nao foi possivel salvar o video", block: nil)
}
}
}
PS: I used this tutorial as reference -> http://www.mikitamanko.com/blog/2017/05/21/swift-how-to-record-a-screen-video-or-convert-images-to-videos/
It really looks like this is not possible, at least not the way you are trying to do it.
There are quite a few ways to render a UIViews content into an image, but all of them must be used from the main thread only. This applies also to the drawInHierarchy method you are using.
As you have to call it on the main thread and the method is just getting called so many times, I think this will never work in a performant way.
See profiling in Instruments:
Sources:
How to render view into image faster?
Safe way to render UIVIew to an image on background thread?

How to make audioplayer in swift?

I made audio player. I realize playlist and playing audio background functionality. I need to recognize which audio played in background.
Firts I create audio player object like:
var mp3Player:AVAudioPlayer?=AVAudioPlayer()
var firstLoad=true
var playingType_Index=0
var speedType_Index=0
When I click audio from list I put clicked audio to my selectedAudio
let audios=[
[
"image":UIImage.fontAwesomeIconWithName(.Headphones, textColor: UIColor(colorLiteralRed: 223/255, green: 156/255, blue: 104/255, alpha: 1.0), size: CGSizeMake(35, 35)),
"title": "Audio1",
"desc":"Erkemin",
"time":3,
"src":NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("audio1", ofType: "mp3")!)
]
]
var selectedAudio=[
"id":"",
"status":"",
"image":"",
"title": "",
"desc":"",
"time":0,
"src":""
]
func playAudio(sender:AnyObject){
firstLoad=false
backwardBtn.enabled=true
step_backward.enabled=true
play_pauseBtn.enabled=true
stopBtn.enabled=true
step_forward.enabled=true
forwardBtn.enabled=true
audioSlider.enabled=true
selectedAudio=audios[sender.tag]
selectedAudio["id"]=sender.tag
selectedAudio["status"]="true"
music()
tableView.reloadData()
}
I'm using selectedAUdio to recognize which audio now played. But when go to other view my audio continue playing but my selected audio being nil.
How can I do this?
I think you could have some problem like re-creation of this view: if you launch this view and playAudio(), selectAudio property is filled with your parameters but for something reason seems you re-make this view so your selectedAudio return to default values (empty strings).
If you need to stop your player with his method stop, you could do something like the code below:
func stopMp3Player() {
if let player = mp3Player {
if player.playing {
player.stop()
}
}
}
Warning: when you build this methods (you could also make pause) you should always check if your mp3Player is not nil to avoid crash.
About your play method I would do something like:
func playMp3Player(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
print("Could not find file: \(filename)")
return
}
do { mp3Player = try AVAudioPlayer(contentsOfURL: url!, fileTypeHint: nil) }
catch let error as NSError { print(error.description) }
if let player = mp3Player {
player.volume = // set your volume here
player.numberOfLoops = 0 // set the repeating
player.prepareToPlay()
player.play()
}
}

How to play the same sound overlapping with AVAudioPlayer?

This code plays the sound when the button is tapped but cancels the previous if it is pressed again. I do not want this to happen I want the same sound to overlap when repeatedly pressed. I believe it might be due to using the same AVAudioPlayer as I have looked on the internet but I am new to swift and want to know how to create a new AVAudioPlayer everytime the method runs so the sounds overlap.
func playSound(sound:String){
// Set the sound file name & extension
let soundPath = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(sound, ofType: "mp3")!)
do {
//Preperation
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch _{
}
do {
try AVAudioSession.sharedInstance().setActive(true)
}
catch _ {
}
//Play the sound
var error:NSError?
do{
audioPlayer = try AVAudioPlayer(contentsOfURL: soundPath)
}catch let error1 as NSError {
error = error1
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
To play two sounds simultaneously with AVAudioPlayer you just have to use a different player for each sound.
In my example I've declared two players, playerBoom and playerCrash, in the Viewcontroller, and I'm populating them with a sound to play via a function, then trigger the play at once:
import AVFoundation
class ViewController: UIViewController {
var playerBoom:AVAudioPlayer?
var playerCrash:AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
playerBoom = preparePlayerForSound(named: "sound1")
playerCrash = preparePlayerForSound(named: "sound2")
playerBoom?.prepareToPlay()
playerCrash?.prepareToPlay()
playerBoom?.play()
playerCrash?.play()
}
func preparePlayerForSound(named sound: String) -> AVAudioPlayer? {
do {
if let soundPath = NSBundle.mainBundle().pathForResource(sound, ofType: "mp3") {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
return try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: soundPath))
} else {
print("The file '\(sound).mp3' is not available")
}
} catch let error as NSError {
print(error)
}
return nil
}
}
It works very well but IMO is not suitable if you have many sounds to play. It's a perfectly valid solution for just a few ones, though.
This example is with two different sounds but of course the idea is exactly the same for two identic sounds.
I could not find a solution using just AVAudioPlayer.
Instead, I have found a solution to this problem with a library that is built on top of AVAudioPlayer.
The library allows same sounds to be played overlapped with each other.
https://github.com/adamcichy/SwiftySound