-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
198 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// | ||
// Created by ktiays on 2023/8/13. | ||
// Copyright (c) 2023 ktiays. All rights reserved. | ||
// | ||
|
||
#if os(iOS) | ||
import SwiftUI | ||
import UIKit | ||
import CyanUtils | ||
import CyanExtensions | ||
import ObjectiveC | ||
|
||
@available(iOS 15.0, *) | ||
public struct EditMenuModifier: ViewModifier { | ||
private let items: () -> [EditMenuAction] | ||
|
||
public init(@ArrayBuilder<EditMenuAction> items: @escaping () -> [EditMenuAction]) { | ||
self.items = items | ||
} | ||
|
||
public func body(content: Content) -> some View { | ||
content.overlay { | ||
GeometryReader { proxy in | ||
Color.clear | ||
.contentShape(Rectangle()) | ||
.onTapGesture { | ||
// If empty `onTapGesture` block is not added, the outer `List` will not be able to scroll. | ||
// This may be a bug in SwiftUI. | ||
} | ||
.onLongPressGesture { | ||
let items = items() | ||
if items.isEmpty { return } | ||
|
||
let menuController = UIMenuController.shared | ||
let dummyView = editMenuDummyView() | ||
dummyView.removeFromSuperview() | ||
guard let keyWindow = UIApplication.shared.cyan.keyWindow else { | ||
return | ||
} | ||
dummyView.frame = proxy.frame(in: .global) | ||
dummyView.actionHandler = { index in | ||
if index >= items.count { return } | ||
items[index].action() | ||
} | ||
keyWindow.addSubview(dummyView) | ||
dummyView.becomeFirstResponder() | ||
|
||
menuController.menuItems = items.enumerated().map { (index, action) in | ||
return .init( | ||
title: action.title ?? "", | ||
action: _DummyView.selector(for: index) | ||
) | ||
} | ||
menuController.showMenu(from: dummyView, rect: dummyView.bounds) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private func editMenuDummyView() -> _DummyView { | ||
if let view = UIMenuController.shared.dummyView { | ||
return view | ||
} | ||
let view = _DummyView() | ||
UIMenuController.shared.dummyView = view | ||
return view | ||
} | ||
|
||
fileprivate class _DummyView: UIView { | ||
static var key: Void? = nil | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
self.isUserInteractionEnabled = false | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
static let prefix = "CALL_WITH_INDEX_" | ||
private static let prefixLength = prefix.count | ||
|
||
static func selector(for index: Int) -> Selector { | ||
Selector("\(prefix)\(index)") | ||
} | ||
|
||
var actionHandler: ((Int) -> Void)? | ||
|
||
class func willRespond(to selector: Selector) -> Bool { | ||
NSStringFromSelector(selector).hasPrefix(prefix) | ||
} | ||
|
||
override class func resolveInstanceMethod(_ selector: Selector!) -> Bool { | ||
let name = NSStringFromSelector(selector) | ||
|
||
// intercept unknown selectors of the form `callWithIndex<int>` | ||
guard name.hasPrefix(prefix), let index = Int(name.dropFirst(prefixLength)) else { | ||
return super.resolveInstanceMethod(selector) | ||
} | ||
|
||
// add a new method that calls the handler with the given index | ||
let imp: @convention(block) (Self) -> Void = { instance in | ||
instance.actionHandler?(index) | ||
instance.resignFirstResponder() | ||
} | ||
|
||
// types "v@:" -> returns void (v), takes object (@) and selector (:) | ||
return class_addMethod(self, selector, imp_implementationWithBlock(imp), "v@:") | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 15.0, *) | ||
private extension UIMenuController { | ||
var dummyView: EditMenuModifier._DummyView? { | ||
set { | ||
objc_setAssociatedObject(self, &EditMenuModifier._DummyView.key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | ||
} | ||
get { | ||
objc_getAssociatedObject(self, &EditMenuModifier._DummyView.key) as? EditMenuModifier._DummyView | ||
} | ||
} | ||
} | ||
|
||
@available(iOS 15.0, *) | ||
public struct EditMenuAction { | ||
public let title: String? | ||
public let action: () -> Void | ||
|
||
public init(_ title: String?, action: @escaping () -> Void) { | ||
self.title = title | ||
self.action = action | ||
} | ||
} | ||
|
||
@available(iOS 15.0, *) | ||
public extension View { | ||
func editMenu(@ArrayBuilder<EditMenuAction> _ actions: @escaping () -> [EditMenuAction]) -> some View { | ||
modifier(EditMenuModifier(items: actions)) | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// Created by ktiays on 2023/8/13. | ||
// Copyright (c) 2023 ktiays. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
@resultBuilder | ||
public struct ArrayBuilder<Element> { | ||
public static func buildBlock(_ components: Element...) -> [Element] { | ||
components | ||
} | ||
|
||
public static func buildBlock(_ componentGroups: [Element]...) -> [Element] { | ||
componentGroups.flatMap { $0 } | ||
} | ||
|
||
public static func buildEither(first component: [Element]) -> [Element] { | ||
component | ||
} | ||
|
||
public static func buildEither(second component: [Element]) -> [Element] { | ||
component | ||
} | ||
|
||
public static func buildOptional(_ component: [Element]?) -> [Element] { | ||
component ?? [] | ||
} | ||
|
||
public static func buildArray(_ components: [[Element]]) -> [Element] { | ||
components.flatMap { $0 } | ||
} | ||
|
||
public static func buildExpression(_ expression: Element) -> [Element] { | ||
[expression] | ||
} | ||
|
||
public static func buildLimitedAvailability(_ component: [Element]) -> [Element] { | ||
component | ||
} | ||
|
||
public static func buildFinalResult(_ component: [Element]) -> [Element] { | ||
component | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// | ||
// Created by ktiays on 2023/8/13. | ||
// Copyright (c) 2023 ktiays. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
|