Каждому视图повсплывающемуменю

Всегдаприятно,когдавприложениипродуманымелочи。 Однимизтакихнебаловажныхэлементовинтерфейсаявляетсявсплывающееменю 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

NSObjectforwardInvocation:的实现只需调用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中,它是第一个响应对象