diff --git a/AttributedString.podspec b/AttributedString.podspec index 25ab618..5fa0f9a 100644 --- a/AttributedString.podspec +++ b/AttributedString.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "AttributedString" -s.version = "2.2.2" +s.version = "3.0.0" s.summary = "基于Swift字符串插值快速构建你想要的富文本, 支持点击按住等事件获取, 支持多种类型过滤" s.homepage = "https://github.com/lixiang1994/AttributedString" diff --git a/AttributedString.xcodeproj/project.pbxproj b/AttributedString.xcodeproj/project.pbxproj index 7bd7738..19e4b55 100644 --- a/AttributedString.xcodeproj/project.pbxproj +++ b/AttributedString.xcodeproj/project.pbxproj @@ -833,7 +833,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -861,7 +861,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -888,7 +888,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -915,7 +915,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -943,7 +943,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -967,7 +967,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Demo-Mac/Demo-Mac.xcodeproj/project.pbxproj b/Demo-Mac/Demo-Mac.xcodeproj/project.pbxproj index 5ead072..f8daa17 100644 --- a/Demo-Mac/Demo-Mac.xcodeproj/project.pbxproj +++ b/Demo-Mac/Demo-Mac.xcodeproj/project.pbxproj @@ -7,24 +7,24 @@ objects = { /* Begin PBXBuildFile section */ - 9B34BD5C243DC33500932E6C /* AttributedString.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B34BD58243DC2F900932E6C /* AttributedString.framework */; }; - 9B34BD5D243DC33500932E6C /* AttributedString.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B34BD58243DC2F900932E6C /* AttributedString.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9B34BD6B243F16EE00932E6C /* TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B34BD6A243F16EE00932E6C /* TableViewCell.swift */; }; 9B34BD6D243F172E00932E6C /* AllViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B34BD6C243F172E00932E6C /* AllViewController.swift */; }; 9B6E89C123827C48009EBEBE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6E89C023827C48009EBEBE /* AppDelegate.swift */; }; 9B6E89C323827C48009EBEBE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6E89C223827C48009EBEBE /* ViewController.swift */; }; 9B6E89C523827C49009EBEBE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9B6E89C423827C49009EBEBE /* Assets.xcassets */; }; 9B6E89C823827C49009EBEBE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B6E89C623827C49009EBEBE /* Main.storyboard */; }; + 9BF2B27C27DA1B6900CE59D9 /* AttributedString.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B34BD58243DC2F900932E6C /* AttributedString.framework */; }; + 9BF2B27D27DA1B6900CE59D9 /* AttributedString.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B34BD58243DC2F900932E6C /* AttributedString.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ - 9B34BD5E243DC33500932E6C /* Embed Frameworks */ = { + 9BF2B27E27DA1B6A00CE59D9 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 9B34BD5D243DC33500932E6C /* AttributedString.framework in Embed Frameworks */, + 9BF2B27D27DA1B6900CE59D9 /* AttributedString.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -49,7 +49,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9B34BD5C243DC33500932E6C /* AttributedString.framework in Frameworks */, + 9BF2B27C27DA1B6900CE59D9 /* AttributedString.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -114,7 +114,7 @@ 9B6E89B923827C48009EBEBE /* Sources */, 9B6E89BA23827C48009EBEBE /* Frameworks */, 9B6E89BB23827C48009EBEBE /* Resources */, - 9B34BD5E243DC33500932E6C /* Embed Frameworks */, + 9BF2B27E27DA1B6A00CE59D9 /* Embed Frameworks */, ); buildRules = ( ); @@ -314,10 +314,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Demo-Mac/Demo_Mac.entitlements"; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Demo-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -336,10 +336,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Demo-Mac/Demo_Mac.entitlements"; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = B9D8DJR5J5; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Demo-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Demo/Demo/Details/ActionViewController.swift b/Demo/Demo/Details/ActionViewController.swift index 97fbd3a..ae8607c 100644 --- a/Demo/Demo/Details/ActionViewController.swift +++ b/Demo/Demo/Details/ActionViewController.swift @@ -52,7 +52,7 @@ class ActionViewController: UIViewController { } label.attributed.text = """ - This is \("Label", .font(.systemFont(ofSize: 50)), .action(clicked)) + This is \("Label", .font(.systemFont(ofSize: 50)), .action(clicked), .action(.press, pressed)) This is a picture -> \(.image(#imageLiteral(resourceName: "huaji"), .custom(size: .init(width: 100, height: 100))), action: clicked) -> Displayed in custom size. diff --git a/Demo/Demo/Details/CheckingViewController.swift b/Demo/Demo/Details/CheckingViewController.swift index 28b219b..01b91c6 100644 --- a/Demo/Demo/Details/CheckingViewController.swift +++ b/Demo/Demo/Details/CheckingViewController.swift @@ -15,9 +15,9 @@ class CheckingViewController: ViewController { super.viewDidLoad() // 添加电话号码类型监听 - container.label.attributed.observe([.phoneNumber], highlights: [.foreground(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1))]) { (result) in + container.label.attributed.observe(.phoneNumber, with: .init(.click, highlights: [.foreground(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1))], with: { (result) in print(result) - } + })) // 添加默认类型监听 container.textView.attributed.observe(highlights: [.foreground(#colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1))]) { (result) in print(result) @@ -42,7 +42,6 @@ class CheckingViewController: ViewController { string.add(attributes: [.foreground(#colorLiteral(red: 0.9529411793, green: 0.6862745285, blue: 0.1333333403, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.phoneNumber]) string.add(attributes: [.foreground(#colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.link]) string.add(attributes: [.foreground(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.date]) - string.add(attributes: [.font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.action]) container.label.attributed.text = string } diff --git a/Sources/Action.swift b/Sources/Action.swift index 8c72399..558b8d6 100644 --- a/Sources/Action.swift +++ b/Sources/Action.swift @@ -36,7 +36,8 @@ extension ASAttributedString { /// 触发回调 let callback: (Result) -> Void - /// 内部处理 + /// 内部使用 + internal var isExternal: Bool = true internal var handle: (() -> Void)? public init(_ trigger: Trigger = .click, highlights: [Highlight] = .defalut, with callback: @escaping (Result) -> Void) { @@ -45,10 +46,11 @@ extension ASAttributedString { self.callback = callback } - init(_ trigger: Trigger = .click, highlights: [Highlight] = .defalut) { + internal init(_ trigger: Trigger, _ highlights: [Highlight], _ callback: @escaping (Result) -> Void) { self.trigger = trigger self.highlights = highlights - self.callback = { _ in } + self.callback = callback + self.isExternal = false } } } diff --git a/Sources/AttributedString.swift b/Sources/AttributedString.swift index 78d208d..cf62e60 100644 --- a/Sources/AttributedString.swift +++ b/Sources/AttributedString.swift @@ -89,6 +89,22 @@ public struct ASAttributedString { return } + #if os(iOS) || os(macOS) + // 合并多个Action + var attributes = attributes + + let actions = attributes.compactMap { + $0.attributes[.action] as? Attribute.Action + } + if !actions.isEmpty { + attributes.removeAll(where: { + $0.attributes.keys.contains(.action) + }) + attributes.append(.init(attributes: [.action: actions])) + } + + #endif + // 获取通用属性 var temp: [NSAttributedString.Key: Any] = [:] attributes.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } diff --git a/Sources/Checking.swift b/Sources/Checking.swift index 1aeba13..190471b 100644 --- a/Sources/Checking.swift +++ b/Sources/Checking.swift @@ -48,7 +48,7 @@ extension ASAttributedString.Checking { /// 正则表达式 case regex(NSAttributedString) #if os(iOS) || os(macOS) - case action(ASAttributedString.Action.Result.Content) + case action([ASAttributedString.Action]) #endif #if !os(watchOS) case attachment(NSTextAttachment) @@ -61,6 +61,31 @@ extension ASAttributedString.Checking { } } +#if os(iOS) || os(macOS) + +extension ASAttributedString.Checking { + + public struct Action { + public typealias Trigger = ASAttributedString.Action.Trigger + public typealias Highlight = ASAttributedString.Action.Highlight + + /// 触发类型 + let trigger: Trigger + /// 高亮属性 + let highlights: [Highlight] + /// 触发回调 + let callback: (Result) -> Void + + public init(_ trigger: Trigger = .click, highlights: [Highlight] = .defalut, with callback: @escaping (Result) -> Void) { + self.trigger = trigger + self.highlights = highlights + self.callback = callback + } + } +} + +#endif + extension ASAttributedString.Checking.Result { public struct Date { @@ -168,15 +193,16 @@ extension ASAttributedString { let substring = value.attributedSubstring(from: match.range) result[match.range] = (checking, .regex(substring)) } - + #if os(iOS) || os(macOS) case .action: - let actions: [NSRange: ASAttributedString.Action] = value.get(.action) - for action in actions where !contains(action.key) { - result[action.key] = (.action, .action(value.get(action.key).content)) + let ranges: [NSRange: [Action]] = value.get(.action) + for range in ranges where !contains(range.key) { + let actions = range.value.filter({ $0.isExternal }) + result[range.key] = (.action, .action(actions)) } #endif - + #if !os(watchOS) case .attachment: let attachments: [NSRange: NSTextAttachment] = value.get(.attachment) diff --git a/Sources/Extension/AppKit/NSTextFieldExtension.swift b/Sources/Extension/AppKit/NSTextFieldExtension.swift index 52f143e..eb8e3bd 100644 --- a/Sources/Extension/AppKit/NSTextFieldExtension.swift +++ b/Sources/Extension/AppKit/NSTextFieldExtension.swift @@ -14,7 +14,7 @@ private var NSGestureRecognizerKey: Void? private var NSEventMonitorKey: Void? private var NSTextFieldTouchedKey: Void? private var NSTextFieldActionsKey: Void? -private var NSTextFieldCheckingsKey: Void? +private var NSTextFieldObserversKey: Void? extension NSTextField: ASAttributedStringCompatible { @@ -32,8 +32,11 @@ extension ASAttributedStringWrapper where Base: NSTextField { // 将当前的高亮属性覆盖到新文本中 替换显示的文本 let temp = NSMutableAttributedString(attributedString: newValue.value) - base.attributedStringValue.get(current.1).forEach { (range, attributes) in - temp.setAttributes(attributes, range: range) + let ranges = current.1.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + base.attributedStringValue.get(range).forEach { (range, attributes) in + temp.setAttributes(attributes, range: range) + } } base.attributedStringValue = temp @@ -65,25 +68,30 @@ extension ASAttributedStringWrapper where Base: NSTextField { /// 添加监听 /// - Parameters: /// - checking: 检查类型 - /// - highlights: 高亮样式 - /// - callback: 触发回调 - public func observe(_ checking: Checking, - highlights: [Highlight] = .defalut, - with callback: @escaping (Checking.Result) -> Void) { - observe([checking], highlights: highlights, with: callback) + /// - action: 检查动作 + public func observe(_ checking: Checking, with action: Checking.Action) { + var temp = base.observers + if var value = temp[checking] { + value.append(action) + temp[checking] = value + + } else { + temp[checking] = [action] + } + base.observers = temp } + /// 添加监听 /// - Parameters: - /// - checkings: 检查类型 + /// - checking: 检查类型 /// - highlights: 高亮样式 /// - callback: 触发回调 - public func observe(_ checkings: [Checking] = .defalut, + public func observe(_ checking: Checking, highlights: [Highlight] = .defalut, with callback: @escaping (Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.1) }) } - base.checkings = temp + observe(checking, with: .init(.click, highlights: highlights, with: callback)) } + /// 添加监听 /// - Parameters: /// - checkings: 检查类型 @@ -91,16 +99,22 @@ extension ASAttributedStringWrapper where Base: NSTextField { /// - callback: 触发回调 public func observe(_ checkings: [Checking] = .defalut, highlights: [Highlight] = .defalut, - with callback: @escaping (NSRange, Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.0, $0.1) }) } - base.checkings = temp + with callback: @escaping (Checking.Result) -> Void) { + checkings.forEach { + observe($0, highlights: highlights, with: callback) + } } + /// 移除监听 /// - Parameter checking: 检查类型 public func remove(checking: Checking) { - base.checkings.removeValue(forKey: checking) + base.observers.removeValue(forKey: checking) + } + /// 移除监听 + /// - Parameter checkings: 检查类型 + public func remove(checkings: [Checking]) { + checkings.forEach { base.observers.removeValue(forKey: $0) } } } @@ -124,31 +138,49 @@ extension ASAttributedStringWrapper where Base: NSTextField { guard let string = string else { return } - // 获取全部动作 - let actions: [NSRange: ASAttributedString.Action] = string.value.get(.action) - // 匹配检查 - let checkings = base.checkings - let temp = checkings.keys + (actions.isEmpty ? [] : [.action]) - string.matching(temp).forEach { (range, checking) in + // 获取当前动作 + base.actions = string.value.get(.action) + // 获取匹配检查 添加检查动作 + let observers = base.observers + string.matching(.init(observers.keys)).forEach { (range, checking) in let (type, result) = checking - switch result { - case .action(let result): - guard var action = actions[range] else { return } - action.handle = { - action.callback(.init(range: range, content: result)) - checkings[type]?.1((range, .action(result))) + if var temp = base.actions[range] { + for action in observers[type] ?? [] { + temp.append( + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } + ) } - base.actions[range] = action + base.actions[range] = temp - default: - guard let value = checkings[type] else { return } - var action = Action(.click, highlights: value.0) - action.handle = { - value.1((range, result)) + } else { + base.actions[range] = observers[type]?.map { action in + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } } - base.actions[range] = action } } + + // 统一为所有动作增加handle闭包 + base.actions = base.actions.reduce(into: [:]) { + let result: Action.Result = string.value.get($1.key) + let actions: [Action] = $1.value.reduce(into: []) { + var temp = $1 + temp.handle = { + temp.callback(result) + } + $0.append(temp) + } + $0[$1.key] = actions + } } /// 设置手势识别 @@ -156,7 +188,8 @@ extension ASAttributedStringWrapper where Base: NSTextField { gestures.forEach { base.removeGestureRecognizer($0) } gestures = [] - Set(base.actions.values.map({ $0.trigger })).forEach { + let triggers = base.actions.values.flatMap({ $0 }).map({ $0.trigger }) + Set(triggers).forEach { switch $0 { case .click: let gesture = NSClickGestureRecognizer(target: base, action: #selector(Base.attributedAction)) @@ -193,7 +226,7 @@ extension NSTextField { fileprivate typealias Action = ASAttributedString.Action fileprivate typealias Checking = ASAttributedString.Checking fileprivate typealias Highlight = ASAttributedString.Action.Highlight - fileprivate typealias Checkings = [Checking: ([Highlight], ((NSRange, Checking.Result)) -> Void)] + fileprivate typealias Observers = [Checking: [Checking.Action]] /// 是否启用Action fileprivate var isActionEnabled: Bool { @@ -201,19 +234,19 @@ extension NSTextField { } /// 触摸信息 - fileprivate var touched: (ASAttributedString, NSRange, Action)? { + fileprivate var touched: (ASAttributedString, [NSRange: [Action]])? { get { associated.get(&NSTextFieldTouchedKey) } set { associated.set(retain: &NSTextFieldTouchedKey, newValue) } } /// 全部动作 - fileprivate var actions: [NSRange: Action] { + fileprivate var actions: [NSRange: [Action]] { get { associated.get(&NSTextFieldActionsKey) ?? [:] } set { associated.set(retain: &NSTextFieldActionsKey, newValue) } } /// 监听信息 - fileprivate var checkings: Checkings { - get { associated.get(&NSTextFieldCheckingsKey) ?? [:] } - set { associated.set(retain: &NSTextFieldCheckingsKey, newValue) } + fileprivate var observers: Observers { + get { associated.get(&NSTextFieldObserversKey) ?? [:] } + set { associated.set(retain: &NSTextFieldObserversKey, newValue) } } @objc @@ -221,15 +254,21 @@ extension NSTextField { let point = convert(event.locationInWindow, from: nil) guard bounds.contains(point), window == event.window else { return } guard isActionEnabled else { return } - guard let (range, action) = matching(point) else { return } + let results = matching(point) + guard !results.isEmpty else { return } let string = attributed.string // 备份当前信息 - touched = (string, range, action) + touched = (string, results) // 设置高亮样式 - var temp: [NSAttributedString.Key: Any] = [:] - action.highlights.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } - self.attributedStringValue = attributedStringValue.reset(range: range) { (attributes) in - attributes.merge(temp, uniquingKeysWith: { $1 }) + let ranges = results.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + var temp: [NSAttributedString.Key: Any] = [:] + results[range]?.first?.highlights.forEach { + temp.merge($0.attributes, uniquingKeysWith: { $1 }) + } + attributedStringValue = attributedStringValue.reset(range: range) { (attributes) in + attributes.merge(temp, uniquingKeysWith: { $1 }) + } } } @@ -250,13 +289,14 @@ fileprivate extension NSTextField { func attributedAction(_ sender: NSGestureRecognizer) { guard sender.state == .ended else { return } guard isActionEnabled else { return } - guard let action = touched?.2 else { return } - guard action.trigger.matching(sender) else { return } - // 点击 回调 - action.handle?() + guard let touched = self.touched else { return } + let actions = touched.1.flatMap({ $0.value }) + for action in actions where action.trigger.matching(sender) { + action.handle?() + } } - func matching(_ point: CGPoint) -> (NSRange, Action)? { + func matching(_ point: CGPoint) -> [NSRange: [Action]] { let attributedString = ASAttributedString(attributedStringValue) // 构建同步Label设置的TextKit @@ -279,15 +319,13 @@ fileprivate extension NSTextField { let index = layoutManager.characterIndexForGlyph(at: glyphIndex) // 通过字形距离判断是否在字形范围内 guard fraction > 0, fraction < 1 else { - return nil + return [:] } // 获取点击的字符串范围和回调事件 - guard - let range = actions.keys.first(where: { $0.contains(index) }), - let action = actions[range] else { - return nil + let ranges = actions.keys.filter({ $0.contains(index) }) + return ranges.reduce(into: [:]) { + $0[$1] = actions[$1] } - return (range, action) } } diff --git a/Sources/Extension/UIKit/UILabel/UILabelExtension.swift b/Sources/Extension/UIKit/UILabel/UILabelExtension.swift index 6bb100f..409edfe 100644 --- a/Sources/Extension/UIKit/UILabel/UILabelExtension.swift +++ b/Sources/Extension/UIKit/UILabel/UILabelExtension.swift @@ -18,7 +18,7 @@ import UIKit private var UIGestureRecognizerKey: Void? private var UILabelTouchedKey: Void? private var UILabelActionsKey: Void? -private var UILabelCheckingsKey: Void? +private var UILabelObserversKey: Void? extension UILabel: ASAttributedStringCompatible { @@ -40,8 +40,11 @@ extension ASAttributedStringWrapper where Base: UILabel { // 将当前的高亮属性覆盖到新文本中 替换显示的文本 let temp = NSMutableAttributedString(attributedString: string.value) - base.attributedText?.get(touched.1).forEach { (range, attributes) in - temp.setAttributes(attributes, range: range) + let ranges = touched.1.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + base.attributedText?.get(range).forEach { (range, attributes) in + temp.setAttributes(attributes, range: range) + } } base.attributedText = temp @@ -78,25 +81,30 @@ extension ASAttributedStringWrapper where Base: UILabel { /// 添加监听 /// - Parameters: /// - checking: 检查类型 - /// - highlights: 高亮样式 - /// - callback: 触发回调 - public func observe(_ checking: Checking, - highlights: [Highlight] = .defalut, - with callback: @escaping (Checking.Result) -> Void) { - observe([checking], highlights: highlights, with: callback) + /// - action: 检查动作 + public func observe(_ checking: Checking, with action: Checking.Action) { + var temp = base.observers + if var value = temp[checking] { + value.append(action) + temp[checking] = value + + } else { + temp[checking] = [action] + } + base.observers = temp } + /// 添加监听 /// - Parameters: - /// - checkings: 检查类型 + /// - checking: 检查类型 /// - highlights: 高亮样式 /// - callback: 触发回调 - public func observe(_ checkings: [Checking] = .defalut, + public func observe(_ checking: Checking, highlights: [Highlight] = .defalut, with callback: @escaping (Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.1) }) } - base.checkings = temp + observe(checking, with: .init(.click, highlights: highlights, with: callback)) } + /// 添加监听 /// - Parameters: /// - checkings: 检查类型 @@ -104,21 +112,21 @@ extension ASAttributedStringWrapper where Base: UILabel { /// - callback: 触发回调 public func observe(_ checkings: [Checking] = .defalut, highlights: [Highlight] = .defalut, - with callback: @escaping (NSRange, Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.0, $0.1) }) } - base.checkings = temp + with callback: @escaping (Checking.Result) -> Void) { + checkings.forEach { + observe($0, highlights: highlights, with: callback) + } } /// 移除监听 /// - Parameter checking: 检查类型 public func remove(checking: Checking) { - base.checkings.removeValue(forKey: checking) + base.observers.removeValue(forKey: checking) } /// 移除监听 /// - Parameter checkings: 检查类型 public func remove(checkings: [Checking]) { - checkings.forEach { base.checkings.removeValue(forKey: $0) } + checkings.forEach { base.observers.removeValue(forKey: $0) } } } @@ -137,31 +145,49 @@ extension ASAttributedStringWrapper where Base: UILabel { guard let string = string else { return } - // 获取全部动作 - let actions: [NSRange: ASAttributedString.Action] = string.value.get(.action) - // 匹配检查 - let checkings = base.checkings - let temp = checkings.keys + (actions.isEmpty ? [] : [.action]) - string.matching(temp).forEach { (range, checking) in + // 获取当前动作 + base.actions = string.value.get(.action) + // 获取匹配检查 添加检查动作 + let observers = base.observers + string.matching(.init(observers.keys)).forEach { (range, checking) in let (type, result) = checking - switch result { - case .action(let result): - guard var action = actions[range] else { return } - action.handle = { - action.callback(.init(range: range, content: result)) - checkings[type]?.1((range, .action(result))) + if var temp = base.actions[range] { + for action in observers[type] ?? [] { + temp.append( + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } + ) } - base.actions[range] = action - - default: - guard let value = checkings[type] else { return } - var action = Action(.click, highlights: value.0) - action.handle = { - value.1((range, result)) + base.actions[range] = temp + + } else { + base.actions[range] = observers[type]?.map { action in + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } } - base.actions[range] = action } } + + // 统一为所有动作增加handle闭包 + base.actions = base.actions.reduce(into: [:]) { + let result: Action.Result = string.value.get($1.key) + let actions: [Action] = $1.value.reduce(into: []) { + var temp = $1 + temp.handle = { + temp.callback(result) + } + $0.append(temp) + } + $0[$1.key] = actions + } } /// 设置手势识别 @@ -170,8 +196,9 @@ extension ASAttributedStringWrapper where Base: UILabel { gestures.forEach { base.removeGestureRecognizer($0) } gestures = [] - - Set(base.actions.values.map({ $0.trigger })).forEach { + + let triggers = base.actions.values.flatMap({ $0 }).map({ $0.trigger }) + Set(triggers).forEach { switch $0 { case .click: let gesture = UITapGestureRecognizer(target: base, action: #selector(Base.attributedAction)) @@ -198,7 +225,7 @@ extension UILabel { fileprivate typealias Action = ASAttributedString.Action fileprivate typealias Checking = ASAttributedString.Checking fileprivate typealias Highlight = ASAttributedString.Action.Highlight - fileprivate typealias Checkings = [Checking: ([Highlight], ((NSRange, Checking.Result)) -> Void)] + fileprivate typealias Observers = [Checking: [Checking.Action]] /// 是否启用Action fileprivate var isActionEnabled: Bool { @@ -206,38 +233,47 @@ extension UILabel { } /// 当前触摸 - fileprivate var touched: (ASAttributedString, NSRange, Action)? { + fileprivate var touched: (ASAttributedString, [NSRange: [Action]])? { get { associated.get(&UILabelTouchedKey) } set { associated.set(retain: &UILabelTouchedKey, newValue) } } /// 全部动作 - fileprivate var actions: [NSRange: Action] { + fileprivate var actions: [NSRange: [Action]] { get { associated.get(&UILabelActionsKey) ?? [:] } set { associated.set(retain: &UILabelActionsKey, newValue) } } /// 监听信息 - fileprivate var checkings: Checkings { - get { associated.get(&UILabelCheckingsKey) ?? [:] } - set { associated.set(retain: &UILabelCheckingsKey, newValue) } + fileprivate var observers: Observers { + get { associated.get(&UILabelObserversKey) ?? [:] } + set { associated.set(retain: &UILabelObserversKey, newValue) } } open override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard isActionEnabled, let string = attributed.text, - let touch = touches.first, - let (range, action) = matching(touch.location(in: self)) else { + let touch = touches.first else { + super.touchesBegan(touches, with: event) + return + } + let results = matching(touch.location(in: self)) + guard !results.isEmpty else { super.touchesBegan(touches, with: event) return } ActionQueue.main.began { // 设置触摸范围内容 - touched = (string, range, action) + touched = (string, results) // 设置高亮样式 - var temp: [NSAttributedString.Key: Any] = [:] - action.highlights.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } - attributedText = string.value.reset(range: range) { (attributes) in - attributes.merge(temp, uniquingKeysWith: { $1 }) + let ranges = results.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + var temp: [NSAttributedString.Key: Any] = [:] + results[range]?.first?.highlights.forEach { + temp.merge($0.attributes, uniquingKeysWith: { $1 }) + } + attributedText = string.value.reset(range: range) { (attributes) in + attributes.merge(temp, uniquingKeysWith: { $1 }) + } } } } @@ -280,16 +316,17 @@ fileprivate extension UILabel { ActionQueue.main.action { [weak self] in guard let self = self else { return } guard self.isActionEnabled else { return } - guard let action = self.touched?.2 else { return } - guard action.trigger.matching(sender) else { return } - // 点击 回调 - action.handle?() + guard let touched = self.touched else { return } + let actions = touched.1.flatMap({ $0.value }) + for action in actions where action.trigger.matching(sender) { + action.handle?() + } } } - func matching(_ point: CGPoint) -> (NSRange, Action)? { + func matching(_ point: CGPoint) -> [NSRange: [Action]] { let text = adaptation(scaledAttributedText ?? synthesizedAttributedText ?? attributedText, with: numberOfLines) - guard let attributedString = ASAttributedString(text) else { return nil } + guard let attributedString = ASAttributedString(text) else { return [:] } // 构建同步Label的TextKit let delegate = UILabelLayoutManagerDelegate(scaledMetrics, with: baselineAdjustment) @@ -328,15 +365,13 @@ fileprivate extension UILabel { let index = layoutManager.characterIndexForGlyph(at: glyphIndex) // 通过字形距离判断是否在字形范围内 guard fraction > 0, fraction < 1 else { - return nil + return [:] } // 获取点击的字符串范围和回调事件 - guard - let range = actions.keys.first(where: { $0.contains(index) }), - let action = actions[range] else { - return nil + let ranges = actions.keys.filter({ $0.contains(index) }) + return ranges.reduce(into: [:]) { + $0[$1] = actions[$1] } - return (range, action) } } diff --git a/Sources/Extension/UIKit/UITextViewExtension.swift b/Sources/Extension/UIKit/UITextViewExtension.swift index 5a3e20e..2a7baf3 100644 --- a/Sources/Extension/UIKit/UITextViewExtension.swift +++ b/Sources/Extension/UIKit/UITextViewExtension.swift @@ -18,7 +18,7 @@ import UIKit private var UIGestureRecognizerKey: Void? private var UITextViewTouchedKey: Void? private var UITextViewActionsKey: Void? -private var UITextViewCheckingsKey: Void? +private var UITextViewObserversKey: Void? private var UITextViewObservationsKey: Void? private var UITextViewAttachmentViewsKey: Void? @@ -40,8 +40,11 @@ extension ASAttributedStringWrapper where Base: UITextView { // 将当前的高亮属性覆盖到新文本中 替换显示的文本 let temp = NSMutableAttributedString(attributedString: newValue.value) - base.attributedText.get(touched.1).forEach { (range, attributes) in - temp.setAttributes(attributes, range: range) + let ranges = touched.1.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + base.attributedText?.get(range).forEach { (range, attributes) in + temp.setAttributes(attributes, range: range) + } } base.attributedText = temp @@ -73,25 +76,30 @@ extension ASAttributedStringWrapper where Base: UITextView { /// 添加监听 /// - Parameters: /// - checking: 检查类型 - /// - highlights: 高亮样式 - /// - callback: 触发回调 - public func observe(_ checking: Checking, - highlights: [Highlight] = .defalut, - with callback: @escaping (Checking.Result) -> Void) { - observe([checking], highlights: highlights, with: callback) + /// - action: 检查动作 + public func observe(_ checking: Checking, with action: Checking.Action) { + var temp = base.observers + if var value = temp[checking] { + value.append(action) + temp[checking] = value + + } else { + temp[checking] = [action] + } + base.observers = temp } + /// 添加监听 /// - Parameters: - /// - checkings: 检查类型 + /// - checking: 检查类型 /// - highlights: 高亮样式 /// - callback: 触发回调 - public func observe(_ checkings: [Checking] = .defalut, + public func observe(_ checking: Checking, highlights: [Highlight] = .defalut, with callback: @escaping (Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.1) }) } - base.checkings = temp + observe(checking, with: .init(.click, highlights: highlights, with: callback)) } + /// 添加监听 /// - Parameters: /// - checkings: 检查类型 @@ -99,18 +107,25 @@ extension ASAttributedStringWrapper where Base: UITextView { /// - callback: 触发回调 public func observe(_ checkings: [Checking] = .defalut, highlights: [Highlight] = .defalut, - with callback: @escaping (NSRange, Checking.Result) -> Void) { - var temp = base.checkings - checkings.forEach { temp[$0] = (highlights, { callback($0.0, $0.1) }) } - base.checkings = temp + with callback: @escaping (Checking.Result) -> Void) { + checkings.forEach { + observe($0, highlights: highlights, with: callback) + } } + /// 移除监听 /// - Parameter checking: 检查类型 public func remove(checking: Checking) { - base.checkings.removeValue(forKey: checking) + base.observers.removeValue(forKey: checking) + } + /// 移除监听 + /// - Parameter checkings: 检查类型 + public func remove(checkings: [Checking]) { + checkings.forEach { base.observers.removeValue(forKey: $0) } } + /// 刷新布局 (如果有ViewAttachment的话) public func layout() { base.layout() @@ -133,31 +148,48 @@ extension ASAttributedStringWrapper where Base: UITextView { private func setupActions(_ string: ASAttributedString) { // 清理原有动作记录 base.actions = [:] - - // 获取全部动作 - let actions: [NSRange: ASAttributedString.Action] = string.value.get(.action) - // 匹配检查 - let checkings = base.checkings - let temp = checkings.keys + (actions.isEmpty ? [] : [.action]) - string.matching(temp).forEach { (range, checking) in + // 获取当前动作 + base.actions = string.value.get(.action) + // 获取匹配检查 添加检查动作 + let observers = base.observers + string.matching(.init(observers.keys)).forEach { (range, checking) in let (type, result) = checking - switch result { - case .action(let result): - guard var action = actions[range] else { return } - action.handle = { - action.callback(.init(range: range, content: result)) - checkings[type]?.1((range, .action(result))) + if var temp = base.actions[range] { + for action in observers[type] ?? [] { + temp.append( + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } + ) } - base.actions[range] = action + base.actions[range] = temp - default: - guard let value = checkings[type] else { return } - var action = Action(.click, highlights: value.0) - action.handle = { - value.1((range, result)) + } else { + base.actions[range] = observers[type]?.map { action in + .init( + action.trigger, + action.highlights + ) { _ in + action.callback(result) + } + } + } + } + + // 统一为所有动作增加handle闭包 + base.actions = base.actions.reduce(into: [:]) { + let result: Action.Result = string.value.get($1.key) + let actions: [Action] = $1.value.reduce(into: []) { + var temp = $1 + temp.handle = { + temp.callback(result) } - base.actions[range] = action + $0.append(temp) } + $0[$1.key] = actions } } @@ -169,7 +201,8 @@ extension ASAttributedStringWrapper where Base: UITextView { gestures.forEach { base.removeGestureRecognizer($0) } gestures = [] - Set(base.actions.values.map({ $0.trigger })).forEach { + let triggers = base.actions.values.flatMap({ $0 }).map({ $0.trigger }) + Set(triggers).forEach { switch $0 { case .click: let gesture = UITapGestureRecognizer(target: base, action: #selector(Base.attributedAction)) @@ -233,7 +266,7 @@ extension UITextView { fileprivate typealias Action = ASAttributedString.Action fileprivate typealias Checking = ASAttributedString.Checking fileprivate typealias Highlight = ASAttributedString.Action.Highlight - fileprivate typealias Checkings = [Checking: ([Highlight], ((NSRange, Checking.Result)) -> Void)] + fileprivate typealias Observers = [Checking: [Checking.Action]] /// 是否启用Action fileprivate var isActionEnabled: Bool { @@ -241,38 +274,47 @@ extension UITextView { } /// 触摸信息 - fileprivate var touched: (ASAttributedString, NSRange, Action)? { + fileprivate var touched: (ASAttributedString, [NSRange: [Action]])? { get { associated.get(&UITextViewTouchedKey) } set { associated.set(retain: &UITextViewTouchedKey, newValue) } } /// 全部动作 - fileprivate var actions: [NSRange: Action] { + fileprivate var actions: [NSRange: [Action]] { get { associated.get(&UITextViewActionsKey) ?? [:] } set { associated.set(retain: &UITextViewActionsKey, newValue) } } /// 监听信息 - fileprivate var checkings: Checkings { - get { associated.get(&UITextViewCheckingsKey) ?? [:] } - set { associated.set(retain: &UITextViewCheckingsKey, newValue) } + fileprivate var observers: Observers { + get { associated.get(&UITextViewObserversKey) ?? [:] } + set { associated.set(retain: &UITextViewObserversKey, newValue) } } open override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard isActionEnabled, - let touch = touches.first, - let (range, action) = matching(touch.location(in: self)) else { + let touch = touches.first else { + super.touchesBegan(touches, with: event) + return + } + let results = matching(touch.location(in: self)) + guard !results.isEmpty else { super.touchesBegan(touches, with: event) return } ActionQueue.main.began { let string = attributed.text // 设置触摸范围内容 - touched = (string, range, action) + touched = (string, results) // 设置高亮样式 - var temp: [NSAttributedString.Key: Any] = [:] - action.highlights.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } - attributedText = string.value.reset(range: range) { (attributes) in - attributes.merge(temp, uniquingKeysWith: { $1 }) + let ranges = results.keys.sorted(by: { $0.length > $1.length }) + for range in ranges { + var temp: [NSAttributedString.Key: Any] = [:] + results[range]?.first?.highlights.forEach { + temp.merge($0.attributes, uniquingKeysWith: { $1 }) + } + attributedText = string.value.reset(range: range) { (attributes) in + attributes.merge(temp, uniquingKeysWith: { $1 }) + } } } } @@ -317,14 +359,15 @@ fileprivate extension UITextView { ActionQueue.main.action { [weak self] in guard let self = self else { return } guard self.isActionEnabled else { return } - guard let action = self.touched?.2 else { return } - guard action.trigger.matching(sender) else { return } - // 点击 回调 - action.handle?() + guard let touched = self.touched else { return } + let actions = touched.1.flatMap({ $0.value }) + for action in actions where action.trigger.matching(sender) { + action.handle?() + } } } - func matching(_ point: CGPoint) -> (NSRange, Action)? { + func matching(_ point: CGPoint) -> [NSRange: [Action]] { // 确保布局 layoutManager.ensureLayout(for: textContainer) @@ -347,15 +390,13 @@ fileprivate extension UITextView { let index = layoutManager.characterIndexForGlyph(at: glyphIndex) // 通过字形距离判断是否在字形范围内 guard fraction > 0, fraction < 1 else { - return nil + return [:] } // 获取点击的字符串范围和回调事件 - guard - let range = actions.keys.first(where: { $0.contains(index) }), - let action = actions[range] else { - return nil + let ranges = actions.keys.filter({ $0.contains(index) }) + return ranges.reduce(into: [:]) { + $0[$1] = actions[$1] } - return (range, action) } }