forked from zelo72/mastodon-ios
feat: [WIP] add mention and hashtag input highlight. Add emoji token replacing logic
This commit is contained in:
parent
36604d150f
commit
92a26b2f73
|
@ -145,6 +145,7 @@
|
||||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||||
|
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */; };
|
||||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
||||||
|
@ -409,6 +410,7 @@
|
||||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
|
||||||
|
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TwitterTextEditor+String.swift"; sourceTree = "<group>"; };
|
||||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
||||||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -672,6 +674,7 @@
|
||||||
children = (
|
children = (
|
||||||
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
|
||||||
|
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */,
|
||||||
);
|
);
|
||||||
path = Vender;
|
path = Vender;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1671,6 +1674,7 @@
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
|
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */,
|
||||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
enum ComposeStatusSection: Equatable, Hashable {
|
enum ComposeStatusSection: Equatable, Hashable {
|
||||||
case repliedTo
|
case repliedTo
|
||||||
|
@ -27,9 +28,10 @@ extension ComposeStatusSection {
|
||||||
for tableView: UITableView,
|
for tableView: UITableView,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
composeKind: ComposeKind
|
composeKind: ComposeKind,
|
||||||
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
||||||
) -> UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
) -> UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||||
UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { [weak textEditorViewTextAttributesDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
case .replyTo(let tootObjectID):
|
case .replyTo(let tootObjectID):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell
|
||||||
|
@ -47,6 +49,7 @@ extension ComposeStatusSection {
|
||||||
cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
|
cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
|
||||||
}
|
}
|
||||||
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
||||||
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
// self size input cell
|
// self size input cell
|
||||||
cell.composeContent
|
cell.composeContent
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
|
|
@ -322,7 +322,7 @@ extension StatusSection {
|
||||||
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
// assertionFailure()
|
||||||
cell.pollCountdownSubscription = nil
|
cell.pollCountdownSubscription = nil
|
||||||
cell.statusView.pollCountdownLabel.text = "-"
|
cell.statusView.pollCountdownLabel.text = "-"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
import KeyboardGuide
|
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
|
@ -44,11 +43,13 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
|
||||||
let composeToolbarView: ComposeToolbarView = {
|
let composeToolbarView: ComposeToolbarView = {
|
||||||
let composeToolbarView = ComposeToolbarView()
|
let composeToolbarView = ComposeToolbarView()
|
||||||
|
composeToolbarView.backgroundColor = .secondarySystemBackground
|
||||||
return composeToolbarView
|
return composeToolbarView
|
||||||
}()
|
}()
|
||||||
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||||
let composeToolbarBackgroundView: UIView = {
|
let composeToolbarBackgroundView: UIView = {
|
||||||
let backgroundView = UIView()
|
let backgroundView = UIView()
|
||||||
|
backgroundView.backgroundColor = .secondarySystemBackground
|
||||||
return backgroundView
|
return backgroundView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -91,8 +92,21 @@ extension ComposeViewController {
|
||||||
composeToolbarView.preservesSuperviewLayoutMargins = true
|
composeToolbarView.preservesSuperviewLayoutMargins = true
|
||||||
composeToolbarView.delegate = self
|
composeToolbarView.delegate = self
|
||||||
|
|
||||||
|
composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
|
||||||
|
composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
|
||||||
|
composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDiffableDataSource(for: tableView, dependency: self)
|
viewModel.setupDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: self,
|
||||||
|
textEditorViewTextAttributesDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
// respond scrollView overlap change
|
// respond scrollView overlap change
|
||||||
view.layoutIfNeeded()
|
view.layoutIfNeeded()
|
||||||
|
@ -208,12 +222,105 @@ extension ComposeViewController {
|
||||||
// MARK: - TextEditorViewTextAttributesDelegate
|
// MARK: - TextEditorViewTextAttributesDelegate
|
||||||
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
|
|
||||||
func textEditorView(_ textEditorView: TextEditorView, updateAttributedString attributedString: NSAttributedString, completion: @escaping (NSAttributedString?) -> Void) {
|
func textEditorView(
|
||||||
// TODO:
|
_ textEditorView: TextEditorView,
|
||||||
|
updateAttributedString attributedString: NSAttributedString,
|
||||||
|
completion: @escaping (NSAttributedString?) -> Void
|
||||||
|
) {
|
||||||
|
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
let string = attributedString.string
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||||
|
|
||||||
|
let stringRange = NSRange(location: 0, length: string.length)
|
||||||
|
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)|#([^\\s]+))")
|
||||||
|
// not accept :$ to force user input space to make emoji take effect
|
||||||
|
let emojiMatches = string.matches(pattern: "(?:(^:|\\s:)([a-zA-Z0-9_]+):\\s)")
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set normal apperance
|
||||||
|
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
|
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
||||||
|
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
||||||
|
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
|
||||||
|
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
|
||||||
|
|
||||||
|
for match in highlightMatches {
|
||||||
|
// hashtag
|
||||||
|
if let name = string.substring(with: match, at: 2) {
|
||||||
|
let attachment: TextAttributes.SuffixedAttachment?
|
||||||
|
switch name {
|
||||||
|
// FIXME:
|
||||||
|
case "person":
|
||||||
|
attachment = .init(size: CGSize(width: 20.0, height: 20.0),
|
||||||
|
attachment: .image(UIImage(systemName: "person")!))
|
||||||
|
default:
|
||||||
|
attachment = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let attachment = attachment {
|
||||||
|
let index = match.range.upperBound - 1
|
||||||
|
attributedString.addAttribute(
|
||||||
|
.suffixedAttachment,
|
||||||
|
value: attachment,
|
||||||
|
range: NSRange(location: index, length: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set highlight
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
|
||||||
|
// See `traitCollectionDidChange(_:)`
|
||||||
|
// set accessibility
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
switch self.traitCollection.accessibilityContrast {
|
||||||
|
case .high:
|
||||||
|
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
|
}
|
||||||
|
for match in emojiMatches {
|
||||||
|
if let name = string.substring(with: match, at: 2) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||||
|
|
||||||
|
// set emoji token invisiable (without upper bounce space)
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.font] = UIFont.systemFont(ofSize: 0.01)
|
||||||
|
let rangeWithoutUpperBounceSpace = NSRange(location: match.range.location, length: match.range.length - 1)
|
||||||
|
attributedString.addAttributes(attributes, range: rangeWithoutUpperBounceSpace)
|
||||||
|
|
||||||
|
// append emoji attachment
|
||||||
|
let attachment = TextAttributes.SuffixedAttachment(
|
||||||
|
size: CGSize(width: 20, height: 20),
|
||||||
|
attachment: .image(UIImage(systemName: "circle")!)
|
||||||
|
)
|
||||||
|
let index = match.range.upperBound - 1
|
||||||
|
attributedString.addAttribute(
|
||||||
|
.suffixedAttachment,
|
||||||
|
value: attachment,
|
||||||
|
range: NSRange(location: index, length: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(attributedString)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - ComposeToolbarViewDelegate
|
// MARK: - ComposeToolbarViewDelegate
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import TwitterTextEditor
|
||||||
|
|
||||||
extension ComposeViewModel {
|
extension ComposeViewModel {
|
||||||
|
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
for tableView: UITableView,
|
for tableView: UITableView,
|
||||||
dependency: NeedsDependency
|
dependency: NeedsDependency,
|
||||||
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
|
||||||
) {
|
) {
|
||||||
diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource(
|
diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource(
|
||||||
for: tableView,
|
for: tableView,
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
composeKind: composeKind
|
composeKind: composeKind,
|
||||||
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
//
|
||||||
|
// String.swift
|
||||||
|
// Example
|
||||||
|
//
|
||||||
|
// Copyright 2021 Twitter, Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
@inlinable
|
||||||
|
var length: Int {
|
||||||
|
(self as NSString).length
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func substring(with range: NSRange) -> String {
|
||||||
|
(self as NSString).substring(with: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
func substring(with result: NSTextCheckingResult, at index: Int) -> String? {
|
||||||
|
guard index < result.numberOfRanges else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let range = result.range(at: index)
|
||||||
|
guard range.location != NSNotFound else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return substring(with: result.range(at: index))
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstMatch(pattern: String,
|
||||||
|
options: NSRegularExpression.Options = [],
|
||||||
|
range: NSRange? = nil) -> NSTextCheckingResult?
|
||||||
|
{
|
||||||
|
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let range = range ?? NSRange(location: 0, length: length)
|
||||||
|
return regularExpression.firstMatch(in: self, options: [], range: range)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matches(pattern: String,
|
||||||
|
options: NSRegularExpression.Options = [],
|
||||||
|
range: NSRange? = nil) -> [NSTextCheckingResult]
|
||||||
|
{
|
||||||
|
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let range = range ?? NSRange(location: 0, length: length)
|
||||||
|
return regularExpression.matches(in: self, options: [], range: range)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue