Skip to content

Instantly share code, notes, and snippets.

@Mercandj
Last active March 11, 2019 13:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Mercandj/4f90d02ef3b7393323863f9be54503f9 to your computer and use it in GitHub Desktop.
Save Mercandj/4f90d02ef3b7393323863f9be54503f9 to your computer and use it in GitHub Desktop.

Swift 4 - List of listeners

Note: I'm an Android developer that trying to create iOS apps. So I may be missing some notions. Sorry for that.

Problem

In many managers and buisness classes, we could want to have a listeners mechanism (sometimes called events) with some notions to register (or addListener), unregister (or removeListener) and notify (or broadcast) all the listeners registered.

So the problem is the following: "How to create an implementation of the following contract bellow".

protocol SearchManager {

    func search(
            input: String
    )

    func getSearchResult(
            input: String
    ) -> SearchResult

    func registerSearchManagerListener(
            listener: SearchManagerListener
    )

    func unregisterSearchManagerListener(
            listener: SearchManagerListener
    )
}

protocol SearchManagerListener : class {

    func onSearchResultChanged()
}

class SearchResult {

    private let input: String
    private let youtubeIds: [String]

    init(
            input: String,
            youtubeIds: [String]
    ) {
        self.input = input
        self.youtubeIds = youtubeIds
    }

    func getInput() -> String {
        return input
    }

    func getYoutubeIds() -> [String] {
        return [String](youtubeIds)
    }
}

Note: We could imagine client of this manager for example an UIViewController with

  • a listener implementation of SearchManagerListener that update an UITableView of search result
  • a method viewDidAppear() overriden that register the listener and synchronize the screen
  • a method viewDidDisappear() overriden that unregister the listener to avoid leaks

Solution 1 - With NSHashTable

If I'm not mistaken, this solution use non swift list (more objectiveC) NSHashTable to contains listeners.

  • Pros: The field that contains lsiteners is a list. So it's conform to the expectation that we could have by the reading of the protocol contract.
  • Cons: Notification of listeners require a cast for each listeners.
import Foundation

class SearchManagerImpl: SearchManager {

    private let networkManager: NetworkManager
    private let videoRepository: VideoRepository
    private var searchResults = [String: SearchResult]()
    private var searchManagerListeners: NSHashTable<AnyObject> = NSHashTable(options: .weakMemory)
    
    init(
            networkManager: NetworkManager,
            videoRepository: VideoRepository
    ) {
        self.networkManager = networkManager
        self.videoRepository = videoRepository
    }

    func search(
            input: String
    ) {
        let queue = DispatchQueue(label: "search-queue")
        queue.async {
            let videos = SearchUtils.searchSync(
                    input: input
            )
            self.videoRepository.put(videos: videos)
            let youtubeIds = Video.extractYoutubeIds(
                    videos: videos
            )
            DispatchQueue.main.async {
                self.searchResults[input] = SearchResult(
                        input: input,
                        youtubeIds: youtubeIds
                )
                self.notifySearchResultChanged()
            }
        }
    }

    func getSearchResult(
            input: String
    ) -> SearchResult {
        let searchResult = searchResults[input]
        if searchResult == nil {
            return SearchResult.init(input: input, youtubeIds: [String]())
        }
        return searchResult!
    }

    func registerSearchManagerListener(
            listener: SearchManagerListener
    ) {
        searchManagerListeners.add(listener)
    }

    func unregisterSearchManagerListener(
            listener: SearchManagerListener
    ) {
        searchManagerListeners.remove(listener)
    }

    private func notifySearchResultChanged() {
        for listener in searchManagerListeners.allObjects {
            (listener as! SearchManagerListener).onSearchResultChanged()
        }
    }
}

Solution 2 - With Map

Here a proposition to implement the listeners list with a map of <Ids, Listeners>:

  • Pros: No need to cast the listeners on notification
  • Const: Map is maybe heavier than NSHashTable
import Foundation

class SearchManagerImpl: SearchManager {

    private let networkManager: NetworkManager
    private let videoRepository: VideoRepository
    private var searchResults = [String: SearchResult]()
    private var searchManagerListeners = [ObjectIdentifier: SearchManagerListener]()

    init(
            networkManager: NetworkManager,
            videoRepository: VideoRepository
    ) {
        self.networkManager = networkManager
        self.videoRepository = videoRepository
    }

    func search(
            input: String
    ) {
        let queue = DispatchQueue(label: "search-queue")
        queue.async {
            let videos = SearchUtils.searchSync(
                    input: input
            )
            self.videoRepository.put(videos: videos)
            let youtubeIds = Video.extractYoutubeIds(
                    videos: videos
            )
            DispatchQueue.main.async {
                self.searchResults[input] = SearchResult(
                        input: input,
                        youtubeIds: youtubeIds
                )
                self.notifySearchResultChanged()
            }
        }
    }

    func getSearchResult(
            input: String
    ) -> SearchResult {
        let searchResult = searchResults[input]
        if searchResult == nil {
            return SearchResult.init(input: input, youtubeIds: [String]())
        }
        return searchResult!
    }

    func registerSearchManagerListener(
            listener: SearchManagerListener
    ) {
        searchManagerListeners[ObjectIdentifier(listener)] = listener
    }

    func unregisterSearchManagerListener(
            listener: SearchManagerListener
    ) {
        searchManagerListeners.removeValue(forKey: ObjectIdentifier(listener))
    }

    private func notifySearchResultChanged() {
        for (_, listener) in searchManagerListeners {
            listener.onSearchResultChanged()
        }
    }
}

Help

Do not hesitate to comment this gist. Any help, comment, idea, will help a lot!


Links

  • The solution 1 is based on the Bowserf commit and the work at MWM.
  • The solution 2 is an adaptation of the John Sundell medium article in my personal project.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment