2
\$\begingroup\$

I'm developing an app that joins tele-conference calls so we use conference numbers a lot. I've got this basic structure to encapsulate the information regarding a conference number.

///A model that represents a teleconference number.
struct ConferenceNumber {

    let iso     : String ///Interntational Standardised 2 letter Abbreviation for country.
    let conferenceProvider: ConferenceProvider ///The company that hosts the conference call the number is for.
    let number  : String ///The phone number
    let toll : Bool ///Whether the number is a paid or free line.
    let country: String

    init(ISO: String, country: String, number:String, provider: ConferenceProvider, toll: Bool){
        self.iso    = ISO
        self.country = ConferenceNumber.localeName(from: ISO) ?? country
        self.number = number
        self.conferenceProvider = provider
        self.toll = toll
    }
}

I'm running in to two problems:

  1. This class if often bypassed in favour of storing the numbers in [String] arrays. I can't think of a nice way to put this in to an object as ConferenceNumber.number doesn't sound right.

  2. The conferenceProvider property has resulted in over a dozen switch statement in our code base which all look similar to the code below. Is it possible to reduce repeating myself this often with protocols or subclasses?

ConferenceProvider enum

///Defines the type of conference call service the meeting uses if any.
public enum ConferenceProvider : String {
    case unknown = "",
    zoom = "Zoom",
    att = "AT&T",
    webex = "WebEx",
    arkadin = "Arkadin"
}

Class with ConferenceProvider switch statements example

import Foundation

///Builds, stores, retrieves and queries conference number directories.
class ConferenceNumberDirectory {

    static let att: [ConferenceNumber] = {
        return buildDirectory(for: .att, from: jsonArray)
    }()

    static let arkadinMeetings: [ConferenceNumber] = {
        return buildDirectory(for: .arkadinMeetings, from: jsonArray)
    }()

    static let webex: [ConferenceNumber] = {
        return buildDirectory(for: .webex, from: jsonArray)
    }()

    static let zoom: [ConferenceNumber] = {
        return buildDirectory(for: .zoom, from: jsonArray)
    }()

    static let allProviders: [ConferenceNumber] = {
        return att + arkadinMeetings + webex + zoom
    }()

    static let attPhoneNumbers: [String] = {
        return ConferenceNumberDirectory.att.map({ $0.number })
    }()

    static let arkadinMeetingsPhoneNumbers: [String] = {
        return ConferenceNumberDirectory.arkadinMeetings.map({ $0.number })
    }()

    static let webexPhoneNumbers: [String] = {
        return ConferenceNumberDirectory.webex.map({ $0.number })
    }()

    static let zoomPhoneNumbers: [String] = {
        return ConferenceNumberDirectory.zoom.map({ $0.number })
    }()

    static let allPhoneNumber: [String] = {
        return ConferenceNumberDirectory.allProviders.map { $0.number }
    }()

    private static let jsonArray: [Any]? = {
        guard let numbersFilePath = Bundle.main.path(forResource: "numbers", ofType:"json") else {
            return nil
        }

        let data = try! Data(contentsOf: URL(fileURLWithPath:numbersFilePath), options: .uncached)

        return try! JSONSerialization.jsonObject(with: data) as? [Any]
    }()

    ///Takes the outputted [Any]? from a JSON file and builds an Array of ConferenceNumber objects suitable for Call In to use.
    static func buildDirectory(for conferenceProvider: ConferenceProvider, from jsonArray: [Any]?) -> [ConferenceNumber] {
        var directory = [ConferenceNumber]()
        do {
            guard let rootJSONArray = jsonArray else {
                throw SerializationError.missing("JSON Root")
            }

            for entry in rootJSONArray {
                guard let dictionary = entry as? [String: Any] else {
                    throw SerializationError.missing("JSON Root")
                }
                guard let isoCode = dictionary["ISO Code"] as? String else {
                    throw SerializationError.missing("isoCode")
                }
                guard let country = dictionary["Country"] as? String else {
                    throw SerializationError.missing("country")
                }

                if  let key = getTollKey(for: conferenceProvider), let tollNumbers = try self.extractNumbers(from: dictionary, forKey: key) {
                    directory.append(contentsOf: tollNumbers.map{ConferenceNumber(ISO: isoCode, country: country, number: $0, provider: conferenceProvider, toll: true) })
                }
                if let key = getTollFreeKey(for: conferenceProvider), let tollFreeNumbers = try self.extractNumbers(from: dictionary, forKey: key) {
                    directory.append(contentsOf: tollFreeNumbers.map{ConferenceNumber(ISO: isoCode, country: country, number: $0, provider: conferenceProvider, toll: false)})
                }
            }
        }
        catch let error {
            print(error)
        }
        return directory
    }

    ///Returns the directory for a given conference provider. Returns all if no numbers exist for this provider.
    static func getDirectory(for conferenceProvider: ConferenceProvider) -> [ConferenceNumber]? {
        switch conferenceProvider {
        case .arkadinMeetings:
            return ConferenceNumberDirectory.arkadinMeetings
        case .att:
            return ConferenceNumberDirectory.att
        case .webex:
            return ConferenceNumberDirectory.webex
        case .zoom:
            return ConferenceNumberDirectory.zoom
        default:
            return ConferenceNumberDirectory.att + ConferenceNumberDirectory.arkadinMeetings + ConferenceNumberDirectory.webex + ConferenceNumberDirectory.zoom
        }
    }

    /**
     Returns a ConferenceNumber that matches the phone number and the country ISO code provided. Returns nil if no number is found.
     - parameter number: The phone number to try and find a match for
     - parameter isoCode: The international standard organizations 2 digit code that represents a country for all known languages

     At present we do not store ConferenceNumber Objects in the settings so we often need to convert the Strings we store back in to their original conference number. Where we to save the object we would still need this as user entered numbers will not contain all ConferenceNumber properties.
     */
    static func findConferenceNumber(usingNumber number: String, andISOCode isoCode: String?, fromDirectory conferenceProvider: ConferenceProvider = .unknown) -> ConferenceNumber? {

        if number.isEmpty || isoCode == nil {
            return nil
        }

        switch conferenceProvider {
        case .att:
            return ConferenceNumberDirectory.att.first(where: {$0.iso == isoCode && $0.number == number})
        case .arkadinMeetings:
            return ConferenceNumberDirectory.arkadinMeetings.first(where: {$0.iso == isoCode &&  $0.number == number})
        case .webex:
            return ConferenceNumberDirectory.webex.first(where: {$0.iso == isoCode &&  $0.number == number})
        case .zoom:
            return ConferenceNumberDirectory.zoom.first(where: {$0.iso == isoCode &&  $0.number == number})
        default:
            let allConferenceNumbers = ConferenceNumberDirectory.att + ConferenceNumberDirectory.arkadinMeetings + ConferenceNumberDirectory.webex + ConferenceNumberDirectory.zoom
            return allConferenceNumbers.first(where: {$0.iso == isoCode && $0.number == number})
        }
    }


    /** 
        Returns all conference numbers that share the same number.
        Similarly as the previous one, findConferenceNumber() but getting all possibilities because we know that some countries share the same number, example: 8884266840
    */
    static func findConferenceNumbers(usingNumber number: String, fromDirectory conferenceProvider: ConferenceProvider? = .unknown) -> [ConferenceNumber] {

        if number.isEmpty {
            return []
        }

        switch conferenceProvider {
        case .att?:
            return ConferenceNumberDirectory.att.filter({ $0.number == number})
        case .arkadinMeetings?:
            return ConferenceNumberDirectory.arkadinMeetings.filter({$0.number == number})
        case .webex?:
            return ConferenceNumberDirectory.webex.filter({ $0.number == number})
        case .zoom?:
            return ConferenceNumberDirectory.zoom.filter({$0.number == number})
        default:
            let allConferenceNumbers = ConferenceNumberDirectory.att + ConferenceNumberDirectory.arkadinMeetings + ConferenceNumberDirectory.webex + ConferenceNumberDirectory.zoom
            return allConferenceNumbers.filter({$0.number == number})
        }
    }

    ///Find the provider for a given number, return unknown if the number does not exist
    ///- warning: This will return the first provider that has the current number seeing as providers don't share numbers.
    static func findProvider(forNumber queriedNumber: String) -> ConferenceProvider {
        return ConferenceNumberDirectory.allProviders.first { $0.number == queriedNumber }?.conferenceProvider ?? .unknown
    }

    ///Returns an array of all country iso codes a queriedNumber is used by.
    static func findCountryISOCodes(forNumber queriedNumber: String) -> [String] {
        return ConferenceNumberDirectory.allProviders.filter { $0.number == queriedNumber }.map({ $0.iso })
    }


    ///Extracts telephone numbers from a given dictionary and key by taking the value as a string then returning an array of each comma seperated component of that string.
    private static func extractNumbers(from dictionary: [String: Any], forKey key: String?) throws -> [String]? {
        guard let jsonKey = key ,let tollNumbersString = dictionary[jsonKey] as? String else {
            throw SerializationError.missing(key ?? "key is nil")
        }
        if tollNumbersString.isEmpty {
            return nil
        }
        return tollNumbersString.components(separatedBy: ",")
    }

    ///Returns the JSON key to access a given ConferenceProviders toll numbers.
    private static func getTollKey(for provider: ConferenceProvider) -> String? {
        switch provider {
        case .att:
            return "AT&T toll"
        case .arkadinMeetings:
            return "arkadin Meetings toll"
        case .webex:
            return "Webex toll"
        case .zoom:
            return "Zoom toll"
        default:
            return nil
        }
    }

    ///Returns the JSON key to access a given ConferenceProviders toll-free numbers.
    private static func getTollFreeKey(for provider: ConferenceProvider) -> String? {
        switch provider {
        case .att:
            return "AT&T toll-free"
        case .arkadinMeetings:
            return "arkadin Meetings toll-free"
        case .webex:
            return "Webex toll-free"
        default:
            return nil
        }
    }
}

Class with ConferenceProvider switch statements Example 2

///  Data class, responsible for storing and retrieving of user settings.

import Foundation

class Settings {
    /// init user defaults with the app group suite to be able to use the same values in the app and in the widget
    static let defaults = SharedGlobals.appGroupDefaults

    static var id: String? {
        get { return self.defaults.string(forKey: "id") }
        set { self.defaults.set(newValue, forKey: "id") }
    }

    static var attNumber: String? {
        get { return self.defaults.string(forKey: "attNumber") }
        set { self.defaults.set(newValue, forKey: "attNumber") }
    }

    static var attCountry: String? {
        get { return self.defaults.string(forKey: "attCountry") }
        set { self.defaults.set(newValue, forKey: "attCountry") }
    }

    static var attISO: String? {
        get { return self.defaults.string(forKey: "attISO") }
        set { self.defaults.set(newValue, forKey: "attISO") }
    }

    static var attHostCodes: [String] {
        get { if let codes =  self.defaults.stringArray(forKey: "hostCodes"){
                return codes
            } else {
                return []
            }
        }
        set { self.defaults.set(newValue, forKey: "hostCodes") }
    }

    ///AT&T participant codes, these are paired to hostcodes (by index) so we can determine which host code to use via parsed participant codes.
    static var attParticipantCodes: [String] {
        get { if let codes = self.defaults.stringArray(forKey: "participantCodes"){
                return codes
            } else {
                return []
            }
        }
        set { self.defaults.set(newValue, forKey: "participantCodes") }
    }

    ///arkadinMeetings (Smartcloud)
    static var arkadinMeetingsCountry: String? {
        get { return self.defaults.string(forKey: "arkadinMeetingsCountry") }
        set { self.defaults.set(newValue, forKey: "arkadinMeetingsCountry") }
    }


    static var arkadinMeetingsNumber: String? {
        get { return self.defaults.string(forKey: "arkadinMeetingsNumber") }
        set { self.defaults.set(newValue, forKey: "arkadinMeetingsNumber") }
    }

    static var arkadinMeetingsISO: String? {
        get { return self.defaults.string(forKey: "arkadinMeetingsISO") }
        set { self.defaults.set(newValue, forKey: "arkadinMeetingsISO") }
    }


    static var arkadinMeetingsModeratorCode: String? {
        get { return self.defaults.string(forKey: "arkadinMeetingsModeratorCode") }
        set { self.defaults.set(newValue, forKey: "arkadinMeetingsModeratorCode") }
    }


    ///WEBEX
    static var webexNumber: String? {
        get { return self.defaults.string(forKey: "webexNumber") }
        set { self.defaults.set(newValue, forKey: "webexNumber") }
    }

    static var webexCountry: String? {
        get { return self.defaults.string(forKey: "webexCountry") }
        set { self.defaults.set(newValue, forKey: "webexCountry") }
    }

    static var webexISO: String? {
        get { return self.defaults.string(forKey: "webexISO") }
        set { self.defaults.set(newValue, forKey: "webexISO") }
    }

    //Webex participant code, this is stored as the participant code is required to dial in even when the user is hosting.
    static var webexAccessCode: String? {
        get { return self.defaults.string(forKey: "webexAccessCode") }
        set { self.defaults.set(newValue, forKey: "webexAccessCode") }
    }

    static var webexHostPin: String? {
        get { return self.defaults.string(forKey: "webexHostPin") }
        set { self.defaults.set(newValue, forKey: "webexHostPin") }
    }


    ///ZOOM
    static var zoomNumber: String? {
        get { return self.defaults.string(forKey: "zoomNumber") }
        set { self.defaults.set(newValue, forKey: "zoomNumber") }
    }

    static var zoomCountry: String? {
        get { return self.defaults.string(forKey: "zoomCountry") }
        set { self.defaults.set(newValue, forKey: "zoomCountry") }
    }

    static var zoomISO: String? {
        get { return self.defaults.string(forKey: "zoomISO")}
        set { self.defaults.set(newValue, forKey: "zoomISO")}
    }

    static var zoomMeetingId: String? {
        get { return self.defaults.string(forKey: "zoomMeetingId") }
        set { self.defaults.set(newValue, forKey: "zoomMeetingId") }
    }


    // MARK: User

    ///Country
    static var country: String? {
        get { return self.defaults.string(forKey: "country") }
        set { self.defaults.set(newValue, forKey: "country") }
    }

    static var email: String? {
        get { return self.defaults.string(forKey: "email") }
        set { self.defaults.set(newValue, forKey: "email") }
    }

    ///Return the user's name. If this hasn't been saved extract their name from their arkadin calendar and save it.
    static var usersName: String? {

        if let savedName = defaults.string(forKey: "name") {
            return savedName
        }

        if let calendar = MeetingsFetcher().getarkadinCalendar().first,
            let parsedName = calendar.title.parseFirstMatch(from: Regex.organizerNameFromEKCalendar)
        {
            defaults.set(parsedName, forKey: "name")
                return parsedName
        }
        return AppConfigInteractor.getUserName()
    }

    static var callMethod: String {
        get {
            if let method = self.defaults.string(forKey: "callMethod") {
                return method
            } else {
                return CallMethod.methods.device.rawValue
            }
        }
        set { self.defaults.set(newValue, forKey: "callMethod") }
    }

    ///VIPS: email + relation for a max of 9 VIPs
    static var VIPemails: [String] {
        get { if let vips = self.defaults.stringArray(forKey: "VIPemails") {
            return vips
        } else {
            return []
            }
        }
        set { self.defaults.set(newValue, forKey: "VIPemails") }
    }

    static var VIPnames: [String] {
        get { if let vips = self.defaults.stringArray(forKey: "VIPnames") {
            return vips
        } else {
            return []
            }
        }
        set { self.defaults.set(newValue, forKey: "VIPnames") }
    }

    static var VIPrelations: [String] {
        get { if let vips =  self.defaults.stringArray(forKey: "VIPrelations"){
            return vips
        } else {
            return []
            }
        }
        set { self.defaults.set(newValue, forKey: "VIPrelations") }
    }


    ///notification
    static var notificationsAllowed: Bool {
        get { return self.defaults.bool(forKey: "notification") }
        set { self.defaults.set(newValue, forKey: "notification") }
    }

    ///location awareness
    static var locationAwarenessAllowed: Bool {
        get { return self.defaults.bool(forKey: "locationAwareness") }
        set { self.defaults.set(newValue, forKey: "locationAwareness") }
    }

    ///promt for authentication awareness
    static var promptForAuthentication: Bool {
        get { return self.defaults.bool(forKey: "promptForAuthentication") }
        set { self.defaults.set(newValue, forKey: "promptForAuthentication") }
    }

    ///last time the user was prompt for authentication
    static var lastAuthenticationPromptDate: Date? {
        get { return self.defaults.object(forKey: "lastAuthenticationPromptDate") as? Date }
        set { self.defaults.set(newValue, forKey: "lastAuthenticationPromptDate") }
    }

    ///audiosummary
    static var audioSummary: Bool {
        get { return self.defaults.bool(forKey: "audioSummary") }
        set { self.defaults.set(newValue, forKey: "audioSummary") }
    }


    /// Local settings (not stored in the cloud)

    static var notifyNumberOfMinutesBeforeEvent: Double {
        get { return  self.defaults.double(forKey: "notifyNumberOfMinutesBeforeEvent") }
        set { self.defaults.set(newValue, forKey: "notifyNumberOfMinutesBeforeEvent") }
    }

    static var landingPageWasReadToday: Bool {
        get { return self.defaults.bool(forKey: "landingPageWasReadToday") }
        set { self.defaults.set(newValue, forKey: "landingPageWasReadToday") }
    }

    static var lastDayAudioWasRead: Int {
        get { return  self.defaults.integer(forKey: "lastDayAudioWasRead") }
        set { self.defaults.set(newValue, forKey: "lastDayAudioWasRead") }
    }

    static var lastDayPopUpWasDismissed: Int {
        get { return  self.defaults.integer(forKey: "lastDayPopUpWasDismissed") }
        set { self.defaults.set(newValue, forKey: "lastDayPopUpWasDismissed") }
    }

    static var meetingsDismissed: [String] {
        get { if let meetings = self.defaults.stringArray(forKey: "meetingsDismissed") {
            return meetings
        } else {
            return []
            }
        }
        set { self.defaults.set(newValue, forKey: "meetingsDismissed") }
    }

    static var promptToModifyNumber: Bool {
        get { return self.defaults.bool(forKey: "promptToModifyNumber") }
        set { self.defaults.set(newValue, forKey: "promptToModifyNumber") }
    }

    ///returns true if the user has already gone though the onboarding process
    static var didOnboarding: Bool {
        get { return self.defaults.bool(forKey: "didOnboarding") }
        set { self.defaults.set(newValue, forKey: "didOnboarding") }
    }

    static var advancedParticipantCode: String {
        get {
            if let code = self.defaults.string(forKey:"advancedParticipantCode") {
                return code
            } else {
                return SharedGlobals.Call.DEFAULT_PARTICIPANT_CODE
            }
        }
        set { self.defaults.set(newValue, forKey: "advancedParticipantCode") }
    }

    static var advancedHostCode: String {
        get {
            if let code = self.defaults.string(forKey:"advancedHostCode") {
                return code
            } else {
                return SharedGlobals.Call.DEFAULT_HOST_CODE
            }
        }
        set { self.defaults.set(newValue, forKey: "advancedHostCode") }
    }

    static var advancedNoCode: String {
        get {
            if let code = self.defaults.string(forKey: "advancedNoCode") {
                return code
            } else {
                return SharedGlobals.Call.DEFAULT_NO_CODE
            }
        }
        set { self.defaults.set(newValue, forKey: "advancedNoCode") }
    }

    static var versionID: String? {
        get { return self.defaults.string(forKey: "versionID") }
        set { self.defaults.set(newValue, forKey: "versionID") }
    }

    static var buildID: String? {
        get { return self.defaults.string(forKey:"buildID") }
        set { self.defaults.set(newValue, forKey: "buildID") }
    }

    ///always use conference number and ignore number in invitation
    static var prioritizeSettingsNumbers: Bool {
        get { return self.defaults.bool(forKey: "prioritizeSettingsNumbers") }
        set { self.defaults.set(newValue, forKey: "prioritizeSettingsNumbers") }
    }

    ///Returns the settings number for a given provider or the AT&T number if provider is unknown.
    static func phoneNumber(for provider: ConferenceProvider) -> String? {
        switch provider {
        case .unknown:
            return attNumber ?? webexNumber ?? arkadinMeetingsNumber ?? zoomNumber
        case .att:
            return attNumber
        case .arkadinMeetings:
            return arkadinMeetingsNumber
        case .webex:
            return webexNumber
        case .zoom:
            return zoomNumber
        }
    }

    ///Returns the iso code for the current Settings number of a given provider, returns nil if no settings number is set.
    static func isoCodeForSettingsNumber(with provider: ConferenceProvider) -> String? {
        switch provider {
        case .unknown:
            return nil
        case .att:
            return attISO
        case .arkadinMeetings:
            return arkadinMeetingsISO
        case .webex:
            return webexISO
        case .zoom:
            return zoomISO
        }
    }

    /**
     Returns the participant codes for a given provider

     - Note: Returns an empty array for arkadinMeetings as we don't store these participant codes.
     - Note: For unknown providers this returns the participant codes for all meetings other than AT&T
     as the user is unlikely to ever dial in with their own AT&T participant code (they'd use their host code).
     */
    static func participantCodes(for provider: ConferenceProvider) -> [String] {
        switch provider {
        case .unknown:
            return [webexAccessCode, zoomMeetingId].flatMap{$0}
        case .att:
            return attParticipantCodes
        case .arkadinMeetings:
            return []
        case .webex:
            return [webexAccessCode].flatMap{$0}
        case .zoom:
            return [zoomMeetingId].flatMap{$0}
        }
    }

    /**
     Returns the host codes for a given provider

     - Note: Returns an empty array for zoom conference calls as they have no host codes.
     - Note: For unknown providers this returns all hostcodes
     */
    static func hostCodes(for provider: ConferenceProvider) -> [String] {
        switch provider {
        case .unknown:
            return attHostCodes + [arkadinMeetingsModeratorCode ,webexHostPin].flatMap{$0}
        case .att:
            return attHostCodes
        case .arkadinMeetings:
            return [arkadinMeetingsModeratorCode].flatMap{$0}
        case .webex:
            return [webexHostPin].flatMap{$0}
        case .zoom:
            return []
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ What is the significance of knowing the provider? Why would a user care if AT&T provider the conferencing vs. IBM? \$\endgroup\$ – user1118321 Mar 4 '18 at 21:34
  • \$\begingroup\$ @user1118321 We store phone numbers for these different providers which we will want to put in to the meeting if we detect text or URLs specific to that provider. The user can also store settings for each providers where they can select or put their own number in as well as add participant codes/ host codes for that number. Different providers also use different dial codes. \$\endgroup\$ – Declan McKenna Mar 5 '18 at 14:38
  • 1
    \$\begingroup\$ let number : String ///The phone number Imagine if you just named the variable let phoneNumber: String. You wouldn't need a comment. Same with toll -> isTollFree (although that has the inverted meaning) \$\endgroup\$ – Alexander Mar 21 '18 at 18:09
0
\$\begingroup\$

I think you've used the wrong abstraction here. I think that's the cause of all these switch statements.

Use Objects or structs

One thing I notice is that it seems like a ConferenceProvider should be an object, not an enum. If it were an object you could hold the provider's name, a link to the ConferenceNumberDirectory entry for that provider, the toll key and the toll free key. Then most of your switch statements in the first example go away, and it just becomes extracting the information from the passed-in provider. It could look something like this:

struct ConferenceProvider {
    let providerName : String
    let providerNumbers : [ConferenceNumber]
    let tollKey : String
    let tollFreeKey : String

    init(providerName: String, providerNumbers: [ConferenceNumber], tollKey: String, tollFreeKey: String) {
        self.providerName = providerName
        self.providerNumbers = providerNumbers
        self.tollKey = tollKey
        self.tollFreeKey = tollFreeKey
    }
}

You'd then have to change getDirectory(for conferenceProvider: ) to do the lookup by name, or some other method. You'd probably want a Dictionary that mapped provider name to the above ConferenceProvider struct.

\$\endgroup\$
  • \$\begingroup\$ How would we apply this to something like number identification? At present we look for URLs and keywords then use something like meeting.conferenceProvider = .att once we identify the meeting uses AT&T teleconference calls. \$\endgroup\$ – Declan McKenna Mar 6 '18 at 13:40
  • \$\begingroup\$ You would still have that information in the struct I mention above. It would just be meeting.conferenceProvider.providerName. Or if you wanted it to still be an enum I suppose it could be an enum instead of a String. \$\endgroup\$ – user1118321 Mar 6 '18 at 17:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.