Каждому视图повсплывающемуменю
Всегдаприятно,когдавприложениипродуманымелочи。 Однимизтакихнебаловажныхэлементовинтерфейсаявляетсявсплывающееменю UIMenuController中的UIMenuController
。 Вэтойстатьемыразберемся,какработатьсовсплывающимменю,скакимиограниченияеиин
Собираемэлементыменю
Итак,мысобираемсяпоказатьвсплывающиеменюдлянекоторогоUIView。 Дляначалаопределимся,какиеименноэлементыбудетпредлагатьнашеменюпользователю。
UIItemView
。 Черезнеговконтроллерменюпередаетсяотображаемыйтекстидействие,происходящеепривабе
let item = UIMenuItem(title:“ Send”,action: #selector (sendTapped))
Рассмотримпоподробнеевторойпараметр— action
。 Окей,этоселекторнекоторогометода,которыйбудетвызванпривыбореэлементаменю。 Значит,этотметоддолженбытьгде-тореализован,ногдеименно? Еслибыэтобылаобычнаякнопка,тополучателяможноуказатьвметодеaddTarget:
sendButton.addTraget( self ,action: #selector (sendTapped),for:...)
在UIMenuItem上添加UIMenuItem
。 Кудажетогдаотправится action
? Обратимсякдокументации:
未指定目标; 通过响应者链的正常遍历找到合适的目标。
响应链 ? Похоже,преждечемдвигатьсядальше,нампридетсянемногоразобратьсявпроцессеобработиисобы。
响应者链иfirstResponder
UIResponder
,UIUI应用程序,UI控制器中的Любоеприложениеможнопредставитьввидеиерархииобъектовкласса Каждыйтакойобъектспособенполучатьсобытия:нажатия, 运动 -событияили UIControlEvents
,илибообрабатыватьполученноесобытие,либопередаватьегоследующему 应答 “увиерархии。
响应者 Но,преждечемпопасть,событиедолжнобытьполученокем-товпервуюочередь。 ПоэтомудлякаждоговприложениисобытияUIKitопределяетнаиболееподходящийобъектклас firstfirstResponder’омтотобъектстановится 响应者, UIApplicatoin
— 响应者链 。
АлгоритмопределенияfirstResponder’aразличендлясобытийразного酶。 Ксчастью,苹果公司можнонайтиотличнуюстатью, Исейчаснасинтересуетодинконкретныйпункт:
编辑菜单消息:第一响应者是您(或UIKit)指定为第一响应者的对象。
Отлично,значит,случаемыимеемделосfirstResponder’ом,управляемымпосредствомметоsign成为resignFirstResponder()
。
Теперьмыможемвернутьсякзаданиюдействийдлянашегоменю。 ТаккакунасестьконкретныйобъектUIView,首先将лектомехотимпоказатьэтоменю,логичнымоен ДляэтогонампридетсяотнаследоватьсяотUIView,并且可以成为canBecomeFirstResponder
,而котороепоуооfalse:
类 ResponsiveView:UIView {
覆盖 var canBecomeFirstResponder: 布尔 {
返回 真
}
}
Итак,вернемсяквопросуотом,гдедолжныбытьреализованыметоды, UIMenuItem
。 注释,UIKit会在firstResponder’а,ивышепоиерархииresponder’ов中发布。 Реализовыватьбизнес-логикуприложенияпрямовоView —链式самаялучшаяидея
//Элементыменю
//
// UIMenuItem(title:“ Red”,操作:#selector(redTapped))
// UIMenuItem(title:“ Green”,操作:#selector(greenTapped))
// UIMenuItem(title:“ Blue”,操作:#selector(blueTapped))
// 类 ViewController:UIViewController {..... private let targetView = ResponsiveView()..... @objc private func redTapped(){
targetView.backgroundColor = .red
targetView.resignFirstResponder()
} @objc私人函数 greenTapped(){
targetView.backgroundColor = .green
targetView.resignFirstResponder()
} @objc私人函数 blueTapped(){
targetView.backgroundColor = .blue
targetView.resignFirstResponder()
}}
НамвсеененеобходимосделатьtargetView firstResponder’ом,нообэтомчутьпозже。 Покачтоможноотметить,чтовкаждомметодевызывается,它们是resignFirstResponder()
。 Этонеобязательный,нологичныйшаг,таккак targetView
необходимобытьfirstResponder“омтольконавремявызовавсплывающегоменю,поэтомумыснимаемснегоэтуроль,кактолькоменюскрывается。
Встроенныедействия
Длячастоиспользуемыхвовсплывающихменюопераций: 剪切 , 复制 , 粘贴 ,инекоторыхдругих(полныйсписокможнонайтивпротоколе UIResponderStandardEditActions
)элементыменюсоздадутсяавтоматически,еслидобавитьсоответствующиеметодыводиниз 应答 “оввцепочке。 俄语俄语俄语
ViewController中的Добавим,дляпримера, Select :
扩展 ViewController { @objc覆盖func select(_ sender: 任何 ?){
//选择某物
}}
Вызываемменю
Сотдельнымиэлементамименюболее-менеераборались,теперьнаконец-тозаймемсяегопоказом。 Чащевсеговсплывающиеменюпоказываютполонгтапунатребуемыйобъект。 ДобавимGestureRecognizer 和 targetView
:
let longPressRecognizer = UILongPressGestureRecognizer(
目标: 自我 ,
动作: #selector (showMenu(_ :))
)
targetView.addGestureRecognizer(longPressRecognizer)
show菜单的Иобработаемвызовменювменю
@objc私人功能 showMenu( _发送者:UIGestureRecognizer){
守护 sender.state ==。开始其他 { return } let menuController = UIMenuController.shared
守卫 !menuController.isMenuVisible 否则 { 返回 } 守卫 targetView.becomeFirstResponder() 否则 { 返回 } menuController.menuItems = [
UIMenuItem(title:“ Red”,action: #selector (redTapped)),
UIMenuItem(title:“ Green”,action: #selector (greenTapped)),
UIMenuItem(标题:“蓝色”,操作: #selector (blueTapped))
]
menuController.setTargetRect(targetView.frame,在:视图中)
menuController.setMenuVisible( true ,动画: true )
}
Разберемпоподробнее,чтопроисходитвэтомметоде。 Первыеинтересующиенасстрочки:
让 menuController = UIMenuController.shared
保护 !menuController.isMenuVisible 其他 { 返回 }
Мыберем 共享实例 UIMenuController.shared
。 ,тостандартноерешение,когданужноконтролировать,чтонаэкраневсегдабудеттолькоодноменю。 GuardнавторойИменноэтомыипроверяемстроке。
Далееследуетвызовметода,которыйужеобсуждалсявыше, targetView
查看 firstResponder’ом:
守卫 targetView.becomeFirstResponder() else { return }
Далееидетнастройкапоказываемогоменю:мыкладемвполе menuItems
массивэлементов,задаем targetRect
:точнаяпозицияменюнаэкранебудетопределенаотносительно переданноготуда 帧 。 Инаконецвызываетсяанимированныйпоказменю。
Отлично,请сработает! Цельдостигнута…。 Новсегдаестьодно“но”。 Получившийсякоддовольнотруднореиспользовать。 Еслимызахотимвызватьтакоежеменюв другойчастинашегоприложения ,тометоды,селекторыкоторыхпередаютсяв UIItemView
,придетсяреализоватьвдругом 的ViewController的“eзаново。
Решениеммоглабыстатьреализацияэтихметодоввнутри 查看 ипередачаобработкинажатийчерезпублич
class MenuView:ResponsiveView { var onRedTapped:(()-> Void)? @objc私人功能 redTapped(){
onRedTapped?()
}}
Ноитакоерешениедалеконеидеально。 Еслинампотребуетсяменюсдругимнаборомэлементов,толибопридетсяделатьдругойкласс-наследник ResponsiveView
,либореализовыватьв MenuView
методыдлявсехвозможныхэлементовменю,дажееслипоказаныбудутлишьнекоторыеизних。
Однако,выходесть。 Немногошаманствасселекторамиимагии目标Cпозволятнамсоздатьv IEWсвозможностьювызыватьвсплывающееменюлюбогоразмера,ивсе,чтодляэтогонужно – передатьнаборэлементовидействияприихвыборе。
Шаманимсэлементами
Первымделом, UIMenuItem
。 Чтонасвнихнеустраивает? То,чтоонисодержатселекторыметодов,由кутнынужноположитьбизнес-логикуобработенежете。 Давайтеобернемэтулогикув 关闭 ипередадимв 项目 готовыйобработчикнажатия:
class MenuItem:UIMenuItem {
init (标题:String,tapHandler: @escaping ()->无效){
onTap = tapHandler
super.init (title:标题,
行动:选择器(“ menuItem _ \(标题)”))
} fileprivate let onTap:()->无效}
super.init中的Заметьте,какойселектормыпередаем。 Насамомделе,неважно,какуюименнострокумыпередадим,главное—чтобыэтотселекторбылуни。
提示Переносимпоказменю
Теперьначнемсоздаватьуниверсальноерешение: 查看 совстроеннымвсплывающимменю,вызывающимсяпол。 ViewController’ :Дляначала,перенесемвнутрькод,которыйраньшележ:
class LongPressMenuView:UIView { var longPressMenuItems:[MenuItem] = [] 覆盖init (框架:CGRect){
super.init (frame:frame)addGestureRecognizer(longPressGesture)
} 私人懒惰var longPressGesture =
UILongPressGestureRecognizer(
目标: 自我 ,
动作: #selector (showMenu(_ :))
) @objc private func showMenu(_ sender:UIGestureRecognizer){
守卫 sender.state ==。 否则 返回 { return }
让 menuController = UIMenuController.shared
守卫 !menuController.isMenuVisible 否则 { 返回 } 守卫成为firstResponder() 否则 { 返回 } menuController.menuItems = longPressMenuItems
menuController.setTargetRect(bounds,in: self )
menuController.setMenuVisible( true ,动画: true )
}}
使用longPressMenuItems即可完成此操作。Pressлементыменюмытеперьпередаем。 Выглядитнеплохо,но,покачто,менюпростонепоявится,таккакметодыдляaction’овменюненебудутнайд。 Исправимэто,переопределивметодcanPerformAction:
覆盖func canPerformAction(
_动作:选择器tt
withSender sender: Any ?)-> Bool
{
let isMenuAction = longPressMenuItems.contains {
$ 0.action ==操作
}
如果 isMenuAction { return true } 返回 超级 .canPerformAction(action,withSender:sender)
}
ТакмыобманываемUIKit,говоря,чтоможемобработатьдействия,соответствующиеселекторамизMenuItem’ов。 Исистемаверитнам,именюдействительнопоявляется。 Но,насамом-тоделе,никакихметодовнет,и,привыборелюбогоэлементаменю,приложениекрешне
Теперьмыподходимксамомуинтересному:каквызвать 闭包 ,лежащийвMenuItem,вместометода,селекторкото 菜单项,菜单项:п
class LongPressMenuView:UIView { var longPressMenuItems:[MenuItem] = [] {
didSet {
menuItemsMap.removeAll()
用于 longPressMenuItems {
menuItemsMap [item.action] =物品
}
}
} ... 覆盖func canPerformAction(
_动作:选择器,
withSender发件人:是吗?)-> Bool
{
如果 menuItemsMap [action]!= nil { 返回true }
返回 超级 .canPerformAction(action,withSender:sender)
} ..... private var menuItemsMap:[选择器:MenuItem] = [:]}
Вызываем封闭вместометода
Вернемсякосновнойпроевввовововщв
请使用NSObject
。 Таммынаходимидеальногокандидатадлянашихцелей—методforwardInvocation,которыйвызывается,еслизапросит ПереопределениеforwardInvocation已将Instance分配unrecognized selector sent to instance
NSObject
的forwardInvocation:
的实现只需调用doesNotRecognizeSelector:
方法即可; 它不转发任何消息。
Но,ксожалению,методforwardInvocation和Objective-C。 НеужелипридетсявеськодLongPressMenuViewпереноситьтуда? Нет,мыпоступимхитрее。
Преждечемпередатьуправление forwardInvocation
,системавызываетдругойметод,выполняющийсхожуюфункцию: forwardingTarget(for:)
Вотличиеотпервогометода,работающегос NSInvocation
,структурой,хранящейвсюизвестнуюинформациюовызываемомметоде,в forwardingTarget
передаетсялишьселектор:
这种方法使对象有机会在更昂贵的
forwardInvocation:
机械接管之前重定向发送给它的未知消息。
快速forwardingTarget
можноработатьизSwift,поэтомупопробуемпереопределитьэтотметодивызватьвнем 关闭 эл
覆盖func ForwardingTarget(for aSelector:Selector)-> 任何吗? {
如果让 menuItem = menuItemsMap [aSelector] {
menuItem.onTap()
}
返回 超级 .forwardingTarget(for:aSelector)
}
注释: onTap()
对пливебореэлементовменю。 提示:приложениевсеерекрешится。 Иэтологично,таккакмынеперенаправилиселекторнадругойобъект,топослевызоваForward目标управлене。 Избежатьэтогоможнолдшиспособом: forwardingTarget
объект,которыйсможетобрарооатьвсе。
选择器水槽
Итак,наннуженобъект,которыйможетпроглотитьвызовывсехселекторов。 界面工具包:
// SelectorSink.h #import @interface SelectorSink:NSObject @end
Винтерфейсе— NSObject
,从以下位置进行:
// SelectorSink.m#import
#import“ SelectorSink.h” @implementation SelectorSink-( void )forwardInvocation:(NSInvocation *)anInvocation {
回报 ;
}-(NSMethodSignature *)methodSignatureForSelector :( SEL )aSelector {
NSMethodSignature * superResult =
[ super methodSignatureForSelector:aSelector]; 如果(superResult!= nil ){
返回 superResult;
}其他{
return [ self methodSignatureForSelector: @selector (dummy)];
}
}-( void )dummy {} @end
Разберемпопорядку,чтоделаетэтотобъект。 前进转发。 Единственное,чтонамотнеготребуется— чтобыонневызывалfatalError ,已完成,
Второйреализованныйметод— methodSignatureForSelector
。 ТаккакSelectorSinkникакнесвязансклассомUIResponder,мынеможемпереопределитьметодcanPerformAction,отот Номыможемопуститьсяглубже,通过ObjectMethod-C运行时和NSMethodSignature进行NSMethodSignature
,
Результатбудетаналогичным:системарешает,что SelectorSink
можетобработатьлюбойселектор,пытаетсяеговызвать,попадетв forwardInvocation
…изаканчиваетобработкуселектора,таккак forwardInvocation
неприводиткошибке。
Витоге,унасполучилсякласс,которыйможетбезошибокпринятьлюбойселектор。 Такойклассможетпригодитьсянетолькодлянашейзадачи,ноивлюбомдругомместе,
SelectorSink
-ForwardingTarget的LongPressMenuView
(在SelectorSink.h
桥接标题 проекта):
覆盖func ForwardingTarget(for aSelector:Selector)-> 任何吗? {
如果让 menuItem = menuItemsMap [aSelector] {
menuItem.onTap()
返回 SelectorSink()
} 其他 {
返回 超级 .forwardingTarget(for:aSelector)
}
}
Поздравляю,немногомагииимысоздалинаследника UIView
,которомудостаточноснаружипередатьназваниякнопокиобработчики нажатийнаних ,ионпокажетвсплывающееменюлюбогоразмера。 ИменнотакойклассиспользуетсявЯндекс.Картахвовсехслучаях,когданужнопоказатьменю。
使用LongPressMenuView的LongPressMenuView
。 Кромевсегоописанноговыше,在UIMenuControllerWillHideMenu的Notification Center中,它是第一个响应对象