Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  ChatSearchController.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import Combine
import MBProgressHUD
import ThreemaEssentials
import ThreemaMacros
import UIKit

/// Get updates for `ChatSearchController` interactions
protocol ChatSearchControllerDelegate: AnyObject {
    
    /// Selected the message with the passed managed object ID
    ///
    /// If another message is already selected this should have its selection removed. If the item is already selected
    /// it should stay selected.
    ///
    /// - Parameter messageObjectID: Managed object ID of message to select
    func chatSearchController(
        select messageObjectID: NSManagedObjectID,
        highlighting searchText: String,
        in filteredSearchResults: [NSManagedObjectID]
    )
    
    /// Remove selection of the message with the passed managed object ID
    /// - Parameter messageObjectID: Managed object ID of message to remove selection from
    func chatSearchController(removeSelectionFrom messageObjectID: NSManagedObjectID)
    
    /// Show toolbar with the passed toolbar button items
    /// - Parameters:
    ///   - barButtonItems: Toolbar items to show
    ///   - animated: Animate showing?
    func chatSearchController(showToolbarWith barButtonItems: [UIBarButtonItem], animated: Bool)
    
    /// Update toolbar with new passed items
    /// - Parameter barButtonItems: Toolbar items to show
    func chatSearchController(updateToolbarWith barButtonItems: [UIBarButtonItem])
    
    /// Hide toolbar
    /// - Parameter animated: Animate hiding?
    func chatSearchControllerHideToolbar(animated: Bool)
    
    /// Hide the search interface
    func chatSearchControllerHideSearch()
}

/// Controller to manage search in a chat
///
/// Use the `searchBar` to show a search bar in the interface, activate the search  with `activateSearch()` and then use
/// the delegate methods to observe any updates.
///
/// - Note: You need the correct implementation of `definePresentationContext` for the search bar to work. See inline
///         documentation for details.
///
/// ## `definePresentationContext` workaround
///
/// Last update: iOS 16.0b8
///
/// ### Problem
///
/// If you have view controller **A** with a `UISearchBar` managed by a `UISearchViewController` that shows view
/// controller **B** also with a manage search bar on the same `UINavigationBar` it will work if you use
/// `searchController` on the navigation item in both views, otherwise not. The search bar cannot become the first
/// responder in **B**.
///
/// ### Solution
///
/// You need to set `definePresentationContext` to `true` in `viewWillAppear` and to `false` in `viewWillDisappear` in
/// **A** _and_ **B**.
///
/// ### Source
///
/// - https://stackoverflow.com/a/42148381
///
final class ChatSearchController: NSObject {
    
    // MARK: - Public properties
    
    /// Search bar for this controller
    ///
    /// Add this to the view hierarchy where the search bar should appear
    var searchBar: UIView {
        searchController.searchBar
    }
    
    // MARK: - Private properties
    
    private let conversation: ConversationEntity
    private weak var delegate: ChatSearchControllerDelegate?
    private let context: NSManagedObjectContext
    private let entityFetcher: EntityFetcher
    private let entityManager: EntityManager
    
    private let searchQueue = DispatchQueue(label: "ch.threema.chatSearchQueue")
    private var cancellables = Set<AnyCancellable>()
    @Published var searchText: String?
    
    private var starredToken: UISearchToken {
        let token = UISearchToken(icon: UIImage(systemName: "star.fill"), text: #localize("search_token_starred_title"))
        return token
    }
    
    private lazy var chatSearchResultsViewController = ChatSearchResultsViewController(
        delegate: self,
        entityManager: entityManager
    )
    
    private lazy var searchController: UISearchController = {
        let searchController = UISearchController(searchResultsController: chatSearchResultsViewController)
        
        // Otherwise the search bar disappears if we add it the `titleView` of the navigation item
        searchController.hidesNavigationBarDuringPresentation = false
        // We have a custom interaction flow managed via the search bar delegate
        searchController.automaticallyShowsSearchResultsController = false
        
        searchController.searchBar.delegate = self
        searchController.searchResultsUpdater = self
        
        searchController.searchBar.searchTextField.allowsDeletingTokens = true
        searchController.obscuresBackgroundDuringPresentation = false
        
        return searchController
    }()
    
    private var filteredMessageObjectIDs = [NSManagedObjectID]() {
        didSet {
            chatSearchResultsViewController.filteredMessageObjectIDs = filteredMessageObjectIDs
        }
    }

    private var currentResultOffset: Int? {
        didSet {
            // Ask delegate to select new item
            
            guard let currentResultOffset else {
                // Remove previous selection if nothing is selected anymore
                if let oldValue,
                   oldValue < filteredMessageObjectIDs.count {
                    let messageObjectID = filteredMessageObjectIDs[oldValue]
                    delegate?.chatSearchController(removeSelectionFrom: messageObjectID)
                }
                return
            }
            
            let messageObjectID = filteredMessageObjectIDs[currentResultOffset]
            delegate?.chatSearchController(
                select: messageObjectID,
                highlighting: searchText!,
                in: filteredMessageObjectIDs
            )
        }
    }
    
    private var currentToolbarButtonItems: [UIBarButtonItem] {
        let nextResultBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: "chevron.up"),
            style: .plain,
            target: self,
            action: #selector(nextResultBarButtonItemTapped)
        )
        
        let previousResultBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: "chevron.down"),
            style: .plain,
            target: self,
            action: #selector(previousResultBarButtonItemTapped)
        )
        
        let localizedMatchesText: String
        let hasMatches: Bool
        
        // Determinate who is active and what text is shown
        if let currentResultOffset {
            // Disable previous button if there are now previous items
            if currentResultOffset == 0 {
                previousResultBarButtonItem.isEnabled = false
            }
            // Disable next button if there are now next items
            else if currentResultOffset == (filteredMessageObjectIDs.count - 1) {
                nextResultBarButtonItem.isEnabled = false
            }
            
            localizedMatchesText = String.localizedStringWithFormat(
                #localize("chat_search_matches"),
                currentResultOffset + 1,
                filteredMessageObjectIDs.count
            )
            hasMatches = true
        }
        else {
            // No matches. Disable everything.
            nextResultBarButtonItem.isEnabled = false
            previousResultBarButtonItem.isEnabled = false
            localizedMatchesText = #localize("chat_search_no_matches")
            hasMatches = false
        }
        
        let searchInfoBarButtonItem = UIBarButtonItem(
            title: localizedMatchesText,
            style: .plain,
            target: self,
            action: #selector(searchInfoBarButtonItemTapped)
        )
        searchInfoBarButtonItem.isEnabled = hasMatches
        
        return [
            nextResultBarButtonItem,
            previousResultBarButtonItem,
            .flexibleSpace(),
            searchInfoBarButtonItem,
        ]
    }
    
    // MARK: - Lifecycle
    
    /// Create a new chat search controller
    /// - Parameter conversation: ConversationEntity to search messages in
    /// - Parameter delegate: Delegate that gets updated during the use of the search controller
    /// - Parameter entityManager: Entity manager used for search and to fetch results messages
    init(
        for conversation: ConversationEntity,
        delegate: ChatSearchControllerDelegate,
        entityManager: EntityManager = BusinessInjector.ui.entityManager
    ) {
        self.conversation = conversation
        self.delegate = delegate
        self.entityManager = entityManager
        
        // We setup a private background context to allow offloading the majority of the search results fetching
        // onto a background thread.
        let persistenceManager = PersistenceManager(
            appGroupID: AppGroup.groupID(),
            userDefaults: AppGroup.userDefaults(),
            remoteSecretManager: RemoteSecretProvider.remoteSecretManager
        )
        var newContext: ThreemaManagedObjectContext
        do {
            newContext = try DatabaseContext.directBackgroundContext(
                with: persistenceManager.databaseManager.persistentStoreCoordinator
            )
        }
        catch {
            fatalError("\(error)")
        }

        self.context = newContext
        self.entityFetcher = persistenceManager.entityFetcher(with: newContext)

        super.init()
    }
    
    @available(*, unavailable)
    override init() {
        fatalError("No available")
    }
    
    deinit {
        DatabaseContext.removeDirectBackgroundContext(context)
    }
    
    // MARK: - Public functions
    
    /// Activate the search bar text field
    ///
    /// Call this after you added the `searchBar` to your view hierarchy and ensure the it can become first responder.
    func activateSearch() {
        searchController.searchBar.becomeFirstResponder()
        setupSearchResultsFetching()
    }
    
    /// Adds the starred message token to the `searchBar` which is used to filter the messages
    func addStarredToken() {
        searchController.searchBar.searchTextField.insertToken(starredToken, at: 0)
        setupSearchResultsFetching()
    }
    
    // MARK: - Private functions

    // Cache search text and tokens
    private var lastSearchText: String?
    private var lastSearchTokens: [UISearchToken]?

    private func setupSearchResultsFetching() {
        
        cancellables.forEach { $0.cancel() }
        
        $searchText
            .debounce(for: ChatViewConfiguration.SearchResultsFetching.debounceInputSeconds, scheduler: RunLoop.main)
            .receive(on: searchQueue)
            .sink { [weak self] searchText in
                guard let self else {
                    return
                }
                
                guard let searchText else {
                    return
                }

                let tokens: [UISearchToken] = DispatchQueue.main.sync {
                    self.searchController.searchBar.searchTextField.tokens
                }

                // Check has search text or tokens changed
                if let lastSearchText, let lastSearchTokens {
                    guard lastSearchText != searchText || lastSearchTokens != tokens else {
                        return
                    }
                }

                lastSearchText = searchText
                lastSearchTokens = tokens

                DispatchQueue.main.async {
                    let hud = MBProgressHUD.showAdded(to: self.chatSearchResultsViewController.view, animated: true)
                    hud.mode = .indeterminate
                    hud.label.text = #localize("chat_search_searching")
                    hud.removeFromSuperViewOnHide = true
                }

                context.performAndWait {
                    self.filteredMessageObjectIDs = self.entityFetcher.matchingMessages(
                        containing: searchText,
                        starred: !tokens.isEmpty,
                        in: self.conversation,
                        limit: ChatViewConfiguration.SearchResultsFetching.maxItemsToFetch
                    )
                }
                
                DispatchQueue.main.async {
                    MBProgressHUD.hide(for: self.chatSearchResultsViewController.view, animated: true)
                }
                
            }.store(in: &cancellables)
    }
    
    private func showSearchResultsController() {
        delegate?.chatSearchControllerHideToolbar(animated: true)
        searchController.showsSearchResultsController = true
    }
    
    private func hideSearchResultsController(showToolbar: Bool = true) {
        searchController.showsSearchResultsController = false
        if showToolbar {
            // We dismiss the searchResultsController to make the ChatView accessible for VoiceOver users.
            searchController.searchResultsController?.dismiss(animated: false)
            delegate?.chatSearchController(showToolbarWith: currentToolbarButtonItems, animated: true)
        }
    }
    
    // MARK: - Actions
    
    @objc private func previousResultBarButtonItemTapped() {
        guard let currentResultOffset else {
            DDLogVerbose("Failed to jump to next search result. No search result selected.")
            return
        }
        
        self.currentResultOffset = max(currentResultOffset - 1, 0)
        
        delegate?.chatSearchController(updateToolbarWith: currentToolbarButtonItems)
    }
    
    @objc private func nextResultBarButtonItemTapped() {
        guard let currentResultOffset else {
            DDLogVerbose("Failed to jump to previous search result. No search result selected.")
            return
        }
        
        self.currentResultOffset = min(currentResultOffset + 1, filteredMessageObjectIDs.count - 1)
        
        delegate?.chatSearchController(updateToolbarWith: currentToolbarButtonItems)
    }
    
    @objc private func searchInfoBarButtonItemTapped() {
        // We call this instead of `showSearchResultsController()` because this also presents the
        // SearchResultsController again, which might get removed in `hideSearchResultsController()`.
        searchController.searchBar.becomeFirstResponder()
    }
}

// MARK: - UISearchBarDelegate

extension ChatSearchController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        updateSearchResultsVisibility(with: searchText)
    }
    
    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        guard let searchText = searchBar.text else {
            hideSearchResultsController(showToolbar: false)
            return
        }
        
        updateSearchResultsVisibility(with: searchText)
    }
    
    private func updateSearchResultsVisibility(with searchText: String) {
        if searchText.isEmpty, searchController.searchBar.searchTextField.tokens.isEmpty {
            hideSearchResultsController(showToolbar: false)
        }
        else {
            showSearchResultsController()
        }
    }
    
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
        searchController.isActive = false
        currentResultOffset = nil
        filteredMessageObjectIDs = []
        delegate?.chatSearchControllerHideSearch()
    }
}

// MARK: - UISearchResultsUpdating

extension ChatSearchController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        let searchText = searchController.searchBar.text ?? ""
        
        // Cleanup search term
        let strippedAndLowercasedSearchText = searchText.trimmingCharacters(in: .whitespaces).lowercased()
        
        self.searchText = strippedAndLowercasedSearchText
    }
}

// MARK: - ChatSearchResultsViewControllerDelegate

extension ChatSearchController: ChatSearchResultsViewControllerDelegate {
    func chatSearchResults(didSelect messageObjectID: NSManagedObjectID) {
        currentResultOffset = filteredMessageObjectIDs.firstIndex(of: messageObjectID)
        
        hideSearchResultsController(showToolbar: true)
        searchController.searchBar.resignFirstResponder()
    }
}

Messung
CHZG
849810094

¤ Dauer der Verarbeitung: 0.4 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104857
#Domains=438737