UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: How to initialize this subclass

Forums > Swift

I have a subclass SoundWrapper of the class Sound from the SwiftySound framework, in order to add images and make it hashable.

I got it working without the image, but now I need to add the super.init() and it says the required url property is 'private". I tried various ways to do the super.init, but I can't seem to find a proper way.

This is the code for the subclass:

class SoundWrapper:Sound, Hashable{
    let id = UUID()
    let imageName: String

    init(imageName: String) {
        self.imageName = imageName
        super.init(url: url)
    }

    public func hash(into hasher: inout Hasher) {
            hasher.combine(0)
        }

    static func == (lhs: SoundWrapper, rhs: SoundWrapper) -> Bool {
        return lhs.id == rhs.id
    }

}

Which I initialize like this:

let backgroundAudio1 = SoundWrapper(imageName:"background-1", url: getFileUrl(name: "backgroundAudio1"))

And this is the SwiftyFramework:

import Foundation
import AVFoundation

#if os(iOS) || os(tvOS)
/// SoundCategory is a convenient wrapper for AVAudioSessions category constants.
    public enum SoundCategory {

        /// Equivalent of AVAudioSession.Category.ambient.
        case ambient
        /// Equivalent of AVAudioSession.Category.soloAmbient.
        case soloAmbient
        /// Equivalent of AVAudioSession.Category.playback.
        case playback
        /// Equivalent of AVAudioSession.Category.record.
        case record
        /// Equivalent of AVAudioSession.Category.playAndRecord.
        case playAndRecord

        fileprivate var avFoundationCategory: AVAudioSession.Category {
            get {
                switch self {
                case .ambient:
                    return .ambient
                case .soloAmbient:
                    return .soloAmbient
                case .playback:
                    return .playback
                case .record:
                    return .record
                case .playAndRecord:
                    return .playAndRecord
                }
            }
        }
    }
#endif

/// Sound is a class that allows you to easily play sounds in Swift. It uses AVFoundation framework under the hood.
open class Sound {

    // MARK: - Global settings

    /// Number of AVAudioPlayer instances created for every sound. SwiftySound creates 5 players for every sound to make sure that it will be able to play the same sound more than once. If your app doesn't need this functionality, you can reduce the number of players to 1 and reduce memory usage. You can increase the number if your app plays the sound more than 5 times at the same time.
    public static var playersPerSound: Int = 10 {
        didSet {
            stopAll()
            sounds.removeAll()
        }
    }

    #if os(iOS) || os(tvOS)
    /// Sound session. The default value is the shared `AVAudioSession` session.
    public static var session: Session = AVAudioSession.sharedInstance()

    /// Sound category for current session. Using this variable is a convenient way to set AVAudioSessions category. The default value is .ambient.
    public static var category: SoundCategory = {
        let defaultCategory = SoundCategory.ambient
        try? session.setCategory(defaultCategory.avFoundationCategory)
        return defaultCategory
        }() {
        didSet {
            try? session.setCategory(category.avFoundationCategory)
        }
    }
    #endif

    private static var sounds = [URL: Sound]()

    private static let defaultsKey = "com.moonlightapps.SwiftySound.enabled"

    /// Globally enable or disable sound. This setting value is stored in UserDefaults and will be loaded on app launch.
    public static var enabled: Bool = {
        return !UserDefaults.standard.bool(forKey: defaultsKey)
        }() { didSet {
            let value = !enabled
            UserDefaults.standard.set(value, forKey: defaultsKey)
            if value {
                stopAll()
            }
        }
    }

    private let players: [Player]

    private var counter = 0

    /// The class that is used to create `Player` instances. Defaults to `AVAudioPlayer`.
    public static var playerClass: Player.Type = AVAudioPlayer.self

    /// The bundle used to load sounds from filenames. The default value of this property is Bunde.main. It can be changed to load sounds from another bundle.
    public static var soundsBundle: Bundle = .main

    // MARK: - Initialization

    /// Create a sound object.
    ///
    /// - Parameter url: Sound file URL.
    public init?(url: URL) {
        #if os(iOS) || os(tvOS)
            _ = Sound.category
        #endif
        let playersPerSound = max(Sound.playersPerSound, 1)
        var myPlayers: [Player] = []
        myPlayers.reserveCapacity(playersPerSound)
        for _ in 0..<playersPerSound {
            do {
                let player = try Sound.playerClass.init(contentsOf: url)
                myPlayers.append(player)
            } catch {
                print("SwiftySound initialization error: \(error)")
            }
        }
        if myPlayers.count == 0 {
            return nil
        }
        players = myPlayers
        NotificationCenter.default.addObserver(self, selector: #selector(Sound.stopNoteRcv), name: Sound.stopNotificationName, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self, name: Sound.stopNotificationName, object: nil)
    }

    @objc private func stopNoteRcv() {
        stop()
    }

    private static let stopNotificationName = Notification.Name("com.moonlightapps.SwiftySound.stopNotification")

    // MARK: - Main play method

    /// Play the sound.
    ///
    /// - Parameter numberOfLoops: Number of loops. Specify a negative number for an infinite loop. Default value of 0 means that the sound will be played once.
    /// - Returns: If the sound was played successfully the return value will be true. It will be false if sounds are disabled or if system could not play the sound.
    @discardableResult public func play(numberOfLoops: Int = 0, completion: PlayerCompletion? = nil) -> Bool {
        if !Sound.enabled {
            return false
        }
        paused = false
        counter = (counter + 1) % players.count
        let player = players[counter]
        return player.play(numberOfLoops: numberOfLoops, completion: completion)
    }

    // MARK: - Stop playing

    /// Stop playing the sound.
    public func stop() {
        for player in players {
            player.stop()
        }
        paused = false
    }

    /// Pause current playback.
    public func pause() {
        players[counter].pause()
        paused = true
    }

    /// Resume playing.
    @discardableResult public func resume() -> Bool {
        if paused {
            players[counter].resume()
            paused = false
            return true
        }
        return false
    }

    /// Indicates if the sound is currently playing.
    public var playing: Bool {
        return players[counter].isPlaying
    }

    /// Indicates if the sound is paused.
    public private(set) var paused: Bool = false

    // MARK: - Prepare sound

    /// Prepare the sound for playback
    ///
    /// - Returns: True if the sound has been prepared, false in case of error
    @discardableResult public func prepare() -> Bool {
        let nextIndex = (counter + 1) % players.count
        return players[nextIndex].prepareToPlay()
    }

    // MARK: - Convenience static methods

    /// Play sound from a sound file.
    ///
    /// - Parameters:
    ///   - file: Sound file name.
    ///   - fileExtension: Sound file extension.
    ///   - numberOfLoops: Number of loops. Specify a negative number for an infinite loop. Default value of 0 means that the sound will be played once.
    /// - Returns: If the sound was played successfully the return value will be true. It will be false if sounds are disabled or if system could not play the sound.
    @discardableResult public static func play(file: String, fileExtension: String? = nil, numberOfLoops: Int = 0) -> Bool {
        if let url = url(for: file, fileExtension: fileExtension) {
            return play(url: url, numberOfLoops: numberOfLoops)
        }
        return false
    }

    /// Play a sound from URL.
    ///
    /// - Parameters:
    ///   - url: Sound file URL.
    ///   - numberOfLoops: Number of loops. Specify a negative number for an infinite loop. Default value of 0 means that the sound will be played once.
    /// - Returns: If the sound was played successfully the return value will be true. It will be false if sounds are disabled or if system could not play the sound.
    @discardableResult public static func play(url: URL, numberOfLoops: Int = 0) -> Bool {
        if !Sound.enabled {
            return false
        }
        var sound = sounds[url]
        if sound == nil {
            sound = Sound(url: url)
            sounds[url] = sound
        }
        return sound?.play(numberOfLoops: numberOfLoops) ?? false
    }

    /// Stop playing sound for given URL.
    ///
    /// - Parameter url: Sound file URL.
    public static func stop(for url: URL) {
        let sound = sounds[url]
        sound?.stop()
    }

    /// Duration of the sound.
    public var duration: TimeInterval {
        get {
            return players[counter].duration
        }
    }

    /// Sound volume.
    /// A value in the range 0.0 to 1.0, with 0.0 representing the minimum volume and 1.0 representing the maximum volume.
    public var volume: Float {
        get {
            return players[counter].volume
        }
        set {
            for player in players {
                player.volume = newValue
            }
        }
    }

    /// Stop playing sound for given sound file.
    ///
    /// - Parameters:
    ///   - file: Sound file name.
    ///   - fileExtension: Sound file extension.
    public static func stop(file: String, fileExtension: String? = nil) {
        if let url = url(for: file, fileExtension: fileExtension) {
            let sound = sounds[url]
            sound?.stop()
        }
    }

    /// Stop playing all sounds.
    public static func stopAll() {
        NotificationCenter.default.post(name: stopNotificationName, object: nil)
    }

    // MARK: - Private helper method
    private static func url(for file: String, fileExtension: String? = nil) -> URL? {
        return soundsBundle.url(forResource: file, withExtension: fileExtension)
    }

}

/// Player protocol. It duplicates `AVAudioPlayer` methods.
public protocol Player: AnyObject {

    /// Play the sound.
    ///
    /// - Parameters:
    ///   - numberOfLoops: Number of loops.
    ///   - completion: Complation handler.
    /// - Returns: true if the sound was played successfully. False otherwise.
    func play(numberOfLoops: Int, completion: PlayerCompletion?) -> Bool

    /// Stop playing the sound.
    func stop()

    /// Pause current playback.
    func pause()

    /// Resume playing.
    func resume()

    /// Prepare the sound.
    func prepareToPlay() -> Bool

    /// Create a Player for sound url.
    ///
    /// - Parameter url: sound url.
    init(contentsOf url: URL) throws

    /// Duration of the sound.
    var duration: TimeInterval { get }

    /// Sound volume.
    var volume: Float { get set }

    /// Indicates if the player is currently playing.
    var isPlaying: Bool { get }
}

fileprivate var associatedCallbackKey = "com.moonlightapps.SwiftySound.associatedCallbackKey"

public typealias PlayerCompletion = ((Bool) -> ())

extension AVAudioPlayer: Player, AVAudioPlayerDelegate {

    public func play(numberOfLoops: Int, completion: PlayerCompletion?) -> Bool {
        if let cmpl = completion {
            objc_setAssociatedObject(self, &associatedCallbackKey, cmpl, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            self.delegate = self
        }
        self.numberOfLoops = numberOfLoops
        return play()
    }

    public func resume() {
        play()
    }

    public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        let cmpl = objc_getAssociatedObject(self, &associatedCallbackKey) as? PlayerCompletion
        cmpl?(flag)
        objc_removeAssociatedObjects(self)
        self.delegate = nil
    }

    public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
        print("SwiftySound playback error: \(String(describing: error))")
    }

}

#if os(iOS) || os(tvOS)
/// Session protocol. It duplicates `setCategory` method of `AVAudioSession` class.
public protocol Session: AnyObject {
    /// Set category for session.
    ///
    /// - Parameter category: category.
    func setCategory(_ category: AVAudioSession.Category) throws
}

extension AVAudioSession: Session {}
#endif

2      

The only initializer you are showing for your SoundWrapper class looks like this...

init(imageName: String) {
    self.imageName = imageName
    super.init(url: url)
}

It only takes a String as a parameter.

Then, you are saying that you initialize it like this...

let backgroundAudio1 = SoundWrapper(imageName:"background-1", url: getFileUrl(name: "backgroundAudio1"))

Giving it a String and a URL as parameters.

I don't see how this could work.

2      

Hi @Fly0strich,

Yes it is quite likely I am doing something wrong, it is my first time working with subclasses in swift, but as you can see in the example here https://www.hackingwithswift.com/sixty/8/2/class-inheritance the subclass (here SoundWrapper) inherits the url parameter from the superclass Sound, isn't it?

2      

In that example, we have these two classes.

class Dog {
    var name: String
    var breed: String

    init(name: String, breed: String) {
        self.name = name
        self.breed = breed
    }
}
class Poodle: Dog {
    init(name: String) {
        super.init(name: name, breed: "Poodle")
    }
}

If you wanted to initialize a Poodle you would do it like this...

Poodle(name: "Fluffy")

The initializer for Poodle is only set up to take one String as a parameter for its name. The breed doesn't need to be passed in as a parameter, because it always uses "Poodle" as the breed. So, then it can call the super class initializer giving it both of the parameters it needs.

In your project, we basically have these two classes (simplified)

class Sound {
    let url: URL

    init(url: Url) {
        self.url = url
    }
}
class SoundWrapper: Sound, Hashable{
    let imageName: String

    init(imageName: String) {
        self.imageName = imageName
        super.init(url: url)
}

So, when we initialize a SoundWrapper it would look like this...

SoundWrapper(imageName: "background-1")

But then, when it tries to call the initializer for the super class, it is supposed to give it a URL as a parameter, and you are telling it to use url for that. But what is url? We didn't pass it in as a parameter, and we aren't just using a constant value like "Poodle" every time. So, where is it coming from?

Instead, you should probably have something like this...

class SoundWrapper:Sound, Hashable{
    let imageName: String

    init(imageName: String, audioFileName: String) {
        self.imageName = imageName
        super.init(url: getFileUrl(name: audioFileName))
}

Then you could Initialize a SoundWrapper like this...

SoundWrapper(imageName: "background-1", audioFileName: "backgroundAudio1")

3      

Thank you very much for the elaborate explanation, it worked!

I used:

class SoundWrapper:Sound, Hashable{
    let id = UUID()
    let imageName : String

    init(imageName: String, audioFileName: String) {
           self.imageName = imageName
        super.init(url: getFileUrl(name: audioFileName))!
       }

    public func hash(into hasher: inout Hasher) {
            hasher.combine(id)
        }

    static func == (lhs: SoundWrapper, rhs: SoundWrapper) -> Bool {
        return lhs.id == rhs.id
    }

}

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.