iOS 使用 UIMenuController 且不隱藏鍵盤的方法

来源:http://www.cnblogs.com/silence-cnblogs/archive/2017/05/08/6824426.html
-Advertisement-
Play Games

iOS 使用 UIMenuController 且不隱藏鍵盤的方法 在鍵盤顯示的時候使用 UIMenuController 彈出菜單,保持鍵盤顯示且可輸入的狀態。 實現方法有 1. 修改響應鏈(推薦) 2. 遵循 UIKeyInput 協議 3. 自定義 Menu controller 前兩種方法的 ...


iOS 使用 UIMenuController 且不隱藏鍵盤的方法

在鍵盤顯示的時候使用 UIMenuController 彈出菜單,保持鍵盤顯示且可輸入的狀態。

實現方法有

  1. 修改響應鏈(推薦)
  2. 遵循 UIKeyInput 協議
  3. 自定義 Menu controller

前兩種方法的代碼已上傳 GitHub:https://github.com/Silence-GitHub/MenuControllerDemo
第 3 種方法的 GitHub 鏈接:https://github.com/Silence-GitHub/SWMenuController

在此之前,介紹 UIMenuController 的使用方法,以及鍵盤會隱藏的原因。

如果只要實現功能,看第 1 種方法的代碼就可以,正文基本不用看。如果要理解響應鏈(Responder chain)相關的原理,先看 Apple 的文檔 Understanding Responders and the Responder Chain

UIMenuController 的使用方法

自定義一個需要顯示 UIMenuController 的視圖,以 UIButton 為例,自定義類 ShowMenuButton

class ShowMenuButton: UIButton {

    // Return true so that menu controller can display
    override var canBecomeFirstResponder: Bool { return true }
    
    // Return true to show menu for given action
    // Action is in UIResponderStandardEditActions
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return action == #selector(copy(_:))
    }
    
    override func copy(_ sender: Any?) {
        print(#function)
    }
}

ShowMenuButton 必須重載 canBecomeFirstResponder 屬性,返回 true 才能顯示菜單(UIMenuController)。第一響應者(First responder)才能處理菜單,如果 canBecomeFirstResponder 返回 false,不能成為第一響應者,菜單不會顯示。

重載 canPerformAction(_:withSender:) 方法,過濾需要顯示的菜單按鈕(UIMenuItem)。參數 action 有 copy(_:)、paste(_:) 等 UIResponderStandardEditActions 協議的方法。對需要進行的操作返回 true,顯示菜單按鈕(以上代碼顯示“Copy”菜單按鈕);對不需要的操作返回 false,嘗試隱藏菜單按鈕(菜單按鈕不一定隱藏,如果響應鏈中有其他響應者返回 true,此菜單按鈕仍然會顯示)。此方法在預設情況下(沒有實現此方法的時候),如果當前類實現了相應的 action,就會返回 true;如果沒有實現相應的 action,則調用下一個響應者的此方法。如果不實現此方法(或此方法返回 false),響應鏈上有響應者也沒實現此方法(或此方法返回 true)但實現了 copy(_:) 方法,則“Copy”菜單按鈕會顯示。建議實現此方法,至少在響應鏈的這一層控制菜單按鈕。

實現與需要顯示的菜單按鈕對應的 action 方法,以上代碼為 copy(_:) 方法。當菜單按鈕被點擊,action 方法會被髮送。如果沒有實現 canPerformAction(_:withSender:) 方法,UIKit 會沿著響應鏈尋找實現 action 的響應者,把 action 方法發給實現 action 的響應者。一旦實現了 canPerformAction(_:withSender:) 方法且返回 true,action 方法就會發送給當前響應者,不會沿著響應鏈去找實現 action 的響應者,所以必須實現相應的 action 方法。

在控制器(UIViewController)中,讓自定義的 ShowMenuButton 監聽點擊事件

button.addTarget(self, action: #selector(showMenuButtonClicked(_:)), for: .touchUpInside)

點擊 button 彈出菜單

@objc private func showMenuButtonClicked(_ button: UIButton) {
    // Let button become first responder so that menu can display
    button.becomeFirstResponder()
    // Only one UIMenuController instance
    let menu = UIMenuController.shared
    // Custom menu item can perform custom action
    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))
    // Set custom menu item
    menu.menuItems = [customItem]
    // Sets the area in a view above or below which the editing menu is positioned
    menu.setTargetRect(button.frame, in: view)
    // Show menu
    menu.setMenuVisible(true, animated: true)
}

// Custom menu item action
func customItemDidSelect() {
    print(#function)
}

在使用 UIMenuController 之前,使 button 成為第一響應者,菜單才能顯示。

控制器沒有實現 canPerformAction(_:withSender:) 方法,實現了 customItemDidSelect,從 button 開始沿著響應鏈可以找到當前控制器,因此自定義菜單按鈕可以顯示。如果控制器實現 canPerformAction(_:withSender:) 方法且返回 false,則自定義菜單按鈕不會顯示。

如有需要,隱藏菜單

UIMenuController.shared.setMenuVisible(false, animated: true)

註意,UIMenuController 只有一個實例,隱藏後 menuItems 還保留顯示時的值,下次在其他地方顯示還會出現舊的自定義菜單按鈕,因此要在適當的時候更新 menuItems 屬性。

UITextView、UITextField 成為第一響應者(點擊輸入框,準備輸入),鍵盤會顯示。輸入框不是第一響應者,鍵盤會隱藏。由於要顯示菜單的自定義控制項調用 becomeFirstResponder() 方法,成為第一響應者,則輸入框就不是第一響應者,所以鍵盤隱藏。

不隱藏鍵盤的方法

修改響應鏈(推薦)

這是目前最好的方法,代碼量最少。可以正常使用 UIMenuController,並且鍵盤能正常顯示、輸入,輸入框的游標仍然閃爍。

方法思路來自:http://stackoverflow.com/questions/13601643/uimenucontroller-hides-the-keyboard
然而,那些代碼還有 bug,這裡會解決。既然輸入框失去第一響應者,鍵盤會隱藏,那就讓輸入框保持第一響應者。通過改變響應鏈,讓菜單事件傳遞給能處理的響應者。

以 UITextView 為例,自定義類 CustomResponderTextView

class CustomResponderTextView: UITextView {

    weak var overrideNext: UIResponder?
    
    override var next: UIResponder? {
        if let responder = overrideNext { return responder }
        return super.next
    }
    
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        if overrideNext != nil { return false }
        return super.canPerformAction(action, withSender: sender)
    }
}

重載 next 屬性,改變響應鏈。重載 canPerformAction(_:withSender:) 方法,在響應鏈改變時都返回 false。

控制器的代碼需要修改

// Init text view when view did load
var textView: CustomResponderTextView!

@objc private func showMenuButtonClicked(_ button: UIButton) {
    if textView.isFirstResponder {
        // Change responder chain
        textView.overrideNext = button
        // Observe "will hide" to do some cleanup
        // Do not use "did hide" which is not fast enough
        NotificationCenter.default.addObserver(self, selector: #selector(menuControllerWillHide), name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
    } else {
        button.becomeFirstResponder()
    }
    let menu = UIMenuController.shared
    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))
    menu.menuItems = [customItem]
    menu.setTargetRect(button.frame, in: view)
    menu.setMenuVisible(true, animated: true)
}
    
func customItemDidSelect() {
    print(#function)
}
    
@objc private func menuControllerWillHide() {
    // Change responder chain back
    textView.overrideNext = nil
    // Prevent custom menu items from displaying in text view
    UIMenuController.shared.menuItems = nil
    // Remove notification observer
    NotificationCenter.default.removeObserver(self, name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
}

如果 text view 不是第一響應者,鍵盤沒顯示,和原來一樣。如果 text view 是第一響應者,改變響應鏈,讓輸入框的下一個響應者(next)成為 button。菜單要顯示哪些按鈕,從第一響應者 text view 開始,沿著響應鏈,通過 canPerformAction(_:withSender:) 方法判斷。雖然 text view 的 canPerformAction(_:withSender:) 方法返回 false,但 button 的 canPerformAction(_:withSender:) 方法對 copy(_:) 方法返回 true,所以會顯示“Copy”菜單按鈕。點擊“Copy”菜單按鈕,button會執行 copy(_:) 方法。控制器也在這條響應鏈上,實現了 customItemDidSelect 方法,沒實現 canPerformAction(_:withSender:) 方法,則 canPerformAction(_:withSender:) 方法預設對 customItemDidSelect 方法返回 true,所以會顯示自定義菜單按鈕。點擊自定義菜單按鈕,控制器會執行 customItemDidSelect 方法。

監聽菜單消失,在將要消失時,恢復響應鏈,清除自定義菜單按鈕,移除通知監聽。

輸入框自己也可以顯示菜單。如果先點擊 button,然後點擊 text view,讓 text view 顯示菜單,自定義菜單按鈕仍然顯示。因為還沒有監聽菜單消失,所以沒有清除自定義菜單按鈕。因此,監聽鍵盤顯示

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: Notification.Name.UIKeyboardWillShow, object: nil)

在鍵盤將要顯示時清除自定義菜單按鈕,在控制器釋放前移除通知監聽

@objc private func keyboardWillShow() {
    // Prevent custom menu item from displaying in text view
    UIMenuController.shared.menuItems = nil
}

deinit {
    NotificationCenter.default.removeObserver(self)
}

遵循 UIKeyInput 協議

這個方法一定會顯示鍵盤,不能隱藏鍵盤。同時,輸入框的游標不閃爍。一般情況下能正常輸入,但系統中文輸入法只響應部分按鍵(回車、空格等)。

方法思路來自:http://stackoverflow.com/questions/4282964/becomefirstresponder-without-hiding-keyboard/4284675#4284675
在 GitHub 上也有這個方法的代碼示例:https://github.com/jaredsinclair/UIMenuControllerTest
雖然這裡會修複那些代碼的 bug,但輸入框游標不閃爍等問題依然存在。遵循 UIKeyInput 協議的 UIResponder 成為第一響應者,鍵盤就會彈出。

以 UIButton 為例,自定義類 KeyInputButton

protocol KeyInputButtonDelegate: class {
    func keyInputButtonHasText(_ button: KeyInputButton) -> Bool
    func keyInputButton(_ button: KeyInputButton, didInsertText text: String)
    func keyInputButtonDidDeleteBackward(_ button: KeyInputButton)
}

class KeyInputButton: UIButton, UIKeyInput {

    // Return true so that menu controller can display
    override var canBecomeFirstResponder: Bool { return true }
    
    // Return true to show menu for given action
    // Action is in UIResponderStandardEditActions
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return action == #selector(copy(_:))
    }
    
    override func copy(_ sender: Any?) {
        print(#function)
    }
    
    // MARK: - UIKeyInput
    
    weak var delegate: KeyInputButtonDelegate?
    
    var hasText: Bool {
        if let d = delegate {
            return d.keyInputButtonHasText(self)
        }
        return false
    }
    
    // SOGOU, system English, system emoji input method work
    // System Chinese input method typing some characters dose not call this method (but some characters call, e.g "\n" and " ")
    func insertText(_ text: String) {
        delegate?.keyInputButton(self, didInsertText: text)
    }
    
    func deleteBackward() {
        delegate?.keyInputButtonDidDeleteBackward(self)
    }
}

UIKeyInput 協議的方法與鍵盤輸入相關。hasText 方法表示有沒有文本。deleteBackward 方法當鍵盤的刪除鍵點擊時調用。insertText(_:) 方法在鍵盤輸入時調用。讓控制器成為 button 的 delegate,把這些方法傳給 text view (UITextView,不用自定義)

func keyInputButtonHasText(_ button: KeyInputButton) -> Bool {
    return textView.hasText
}

func keyInputButton(_ button: KeyInputButton, didInsertText text: String) {
    textView.insertText(text)
}

func keyInputButtonDidDeleteBackward(_ button: KeyInputButton) {
    textView.deleteBackward()
}

點擊顯示菜單

@objc private func showMenuButtonClicked(_ button: UIButton) {
    button.becomeFirstResponder()
    
    NotificationCenter.default.addObserver(self, selector: #selector(menuControllerWillHide), name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
    
    let menu = UIMenuController.shared
    let customItem = UIMenuItem(title: "Custom item", action: #selector(customItemDidSelect))
    menu.menuItems = [customItem]
    menu.setTargetRect(button.frame, in: view)
    // Display immediately may disappear soon, so display after a little time
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 
        menu.setMenuVisible(true, animated: true)
    }
}
    
func customItemDidSelect() {
    print(#function)
}

@objc private func menuControllerWillHide() {
    // Prevent custom menu items from displaying in text view
    UIMenuController.shared.menuItems = nil
    NotificationCenter.default.removeObserver(self, name: Notification.Name.UIMenuControllerWillHideMenu, object: nil)
}

由於 button 成為第一響應者時鍵盤一定會顯示,所以每次都可以讓 button 調用 becomeFirstResponder 方法。

依然要監聽菜單消失,清除自定義菜單按鈕,移除通知監聽。

需要註意的是,UIMenuController 的 setMenuVisible(_:animated:) 方法要延遲調用,否則菜單可能剛出現就消失。

自定義 Menu controller

由於之前嘗試其他方法不滿意(當時修改響應鏈的方法還有問題),於是查找自定義的菜單。找到一個:https://github.com/camelcc/MenuPopOverView
自己也寫了一個:https://github.com/Silence-GitHub/SWMenuController
以下介紹自己寫的 SWMenuController,先看效果圖

基本夠用,但是和 UIMenuController 還是有差距(例如動畫效果、自動調整字體大小等)。

實現原理是,繼承 UIView,添加 UIButton 作為菜單按鈕,添加到 window 來顯示。

與 UIMenuController 相似,但所有菜單按鈕都要自定義,傳入菜單按鈕標題的數組

let menu = SWMenuController()
menu.delegate = self
menu.menuItems = ["Copy", "Paste", "Select", "Select all", "Look up", "Search", "Delete"]
menu.setTargetRect(frame, in: view)
menu.setMenuVisible(true, animated: true)

實現 SWMenuControllerDelegate 方法,處理第 index 個菜單按鈕的點擊事件(index 從 0 開始)

func menuController(_ menu: SWMenuController, didSelected index: Int) {
    print(menu.menuItems[index])
    // Do something for menu at index
}

轉載請註明出處:http://www.cnblogs.com/silence-cnblogs/p/6824426.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、內聯式css樣式,直接寫在現有的HTML標簽中 1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 5 <title>認識html標簽< ...
  • history.back(-1):直接返回當前頁的上一頁,數據全部消息,是個新頁面 history.go(-1):也是返回當前頁的上一頁,不過表單里的數據全部還在 ...
  • #myCarousel img{ width: 100%; height: 100%; object-fit: cover; } ...
  • 1 /* 遮罩插件 2 * 可選選項 3 * smBoxBg 小方格遮罩顏色 預設 #FFFFFF 4 * backgroudColor 大遮罩顏色 預設 #000000 5 * backgroundImage 動態圖片 預設 loading.gif 6 * text 文字信息 預設 載入中.... ...
  • 背景 很多很多傳統的Web開發者還在用著傳統的jquery和ES5,大家都知道現在的前端如火如荼,但是眼花繚亂的框架和層出不窮的新概念,讓很多人無從下手,本文從0開始,帶你一步步由jquery操作DOM轉型成為一個新思想的前端開發者。沒有過多的引申和概念解釋。先上手實踐,再回頭體會。讓我們開始。(本 ...
  • localStorage是HTML5在在客戶端存儲數據的新方法,存儲的數據沒有時間限制。 localStorage的主要API: localStorage.setItem(key,value); key是保存數據的變數,value是保存的數據 localStorage.getItem(key); 讀 ...
  • 首先可能需要安裝npm,並且配置環境. 1.打開Dos(命令提示符).按Windows徽標鍵+R組合鍵,輸入cmd然後按回車鍵進入Dos. 2.安裝Yeoman.在Dos下輸入npm install -g yo. 3.安裝Grunt.在Dos下輸入npm install -g grunt-cli. ...
  • URL Schemes URL Schemes是蘋果給出的用來跳轉到系統應用或者跳轉到別人的應用的一種機制。同時還可以在應用之間傳數據。 設置一個URL Schemes:選中App工程->Info->URL Types里添加,可以添加多個。 在Info.plist里是這樣的: 打開App的代碼是這樣 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...