diff --git a/.gitignore b/.gitignore index 330d167..c15d801 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,11 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings +.DS_Store +/.build +/Packages xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +codecov +coverage-report-TestResult.json +TestResult.xcresult diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ZNSTextAttachment.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ZNSTextAttachment.xcscheme new file mode 100644 index 0000000..60c38e0 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ZNSTextAttachment.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..2bcfa19 --- /dev/null +++ b/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ZNSTextAttachment", + platforms: [.iOS(.v12), .macOS(.v10_14)], + products: [ + .library(name: "ZNSTextAttachment", targets: ["ZNSTextAttachment"]), + ], + dependencies: [ + ], + targets: [ + .target(name: "ZNSTextAttachment", + path: "Sources"), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..91e8865 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ZNSTextAttachment + +A description of this package. diff --git a/Sources/UIExtensions/ZNSTextAttachmentLabel.swift b/Sources/UIExtensions/ZNSTextAttachmentLabel.swift new file mode 100644 index 0000000..5cc22b5 --- /dev/null +++ b/Sources/UIExtensions/ZNSTextAttachmentLabel.swift @@ -0,0 +1,25 @@ +// +// ZNSTextAttachmentLabel.swift +// +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import Foundation +#if canImport(UIKit) +import UIKit + +public class ZNSTextAttachmentLabel: UILabel { + public override var attributedText: NSAttributedString? { + didSet { + attributedText?.enumerateAttribute(NSAttributedString.Key.attachment, in: NSMakeRange(0, attributedText?.string.utf16.count ?? 0), options: []) { (value, effectiveRange, nil) in + guard let attachment = value as? ZNSTextAttachmentPlaceholder else { + return + } + + attachment.register(label: self) + } + } + } +} +#endif diff --git a/Sources/ZNSTextAttachment/ZNSTextAttachment.swift b/Sources/ZNSTextAttachment/ZNSTextAttachment.swift new file mode 100644 index 0000000..fe7285b --- /dev/null +++ b/Sources/ZNSTextAttachment/ZNSTextAttachment.swift @@ -0,0 +1,237 @@ + +// +// ZNSTextAttachmentLabel.swift +// +// +// Created by https://zhgchg.li on 2023/3/5. +// + +#if canImport(UIKit) +import UIKit +import MobileCoreServices +import UniformTypeIdentifiers + +public protocol ZNSTextAttachmentDataSource: AnyObject { + func zNSTextAttachment(_ textAttachment: ZNSTextAttachmentPlaceholder, loadImageURL imageURL: URL, completion: @escaping (Data) -> Void) +} + +public protocol ZNSTextAttachmentDelegate: AnyObject { + func zNSTextAttachment(didLoad textAttachment: ZNSTextAttachmentPlaceholder) +} + +public class ZNSTextAttachment: NSTextAttachment { + + public let imageSize: CGSize? + + @available(iOS 13.0, *) + public init(image: UIImage) { + imageSize = image.size + super.init(image: image) + } + + public init(imageSize: CGSize?, data: Data, type: String) { + self.imageSize = imageSize + + super.init(data: data, ofType: type) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + + guard let imageWidth = imageSize?.width, + let imageHeight = imageSize?.height else { + return .zero + } + + let maxWidth = lineFrag.size.width - ((textContainer?.lineFragmentPadding ?? 0) * 2) + let factor = maxWidth / imageWidth + + return CGRect(origin: CGPoint.zero, size:CGSize(width: Int(imageWidth * factor), height: Int(imageHeight * factor))) + } +} + +public class ZNSTextAttachmentPlaceholder: NSTextAttachment { + + public let imageURL: URL + public weak var delegate: ZNSTextAttachmentDelegate? + public weak var dataSource: ZNSTextAttachmentDataSource? + + private let origin: CGPoint? + + private var isLoading: Bool = false + private var textStorages: [WeakNSTextStorage] = [] + private var labels: [WeakUILabel] = [] + + public init(imageURL: URL, placeholderImage: UIImage? = nil, placeholderImageOrigin: CGPoint? = nil) { + self.imageURL = imageURL + self.origin = placeholderImageOrigin + + if let placeholderImageData = placeholderImage?.pngData() { + super.init(data: placeholderImageData, ofType: "public.png") + } else { + super.init(data: nil, ofType: nil) + } + + self.image = placeholderImage + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func register(textStorage: NSTextStorage) { + self.appendToTextStorages(with: textStorage) + } + + public func register(label: UILabel) { + self.appendToLabels(with: label) + } + + public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? { + + if let textStorage = textContainer?.layoutManager?.textStorage { + appendToTextStorages(with: textStorage) + } + + guard !isLoading else { return image } + isLoading = true + + dataSource?.zNSTextAttachment(self, loadImageURL: imageURL, completion: { data in + let fileType: String + let pathExtension = self.imageURL.pathExtension + if #available(iOS 14.0, *) { + if let utType = UTType(filenameExtension: pathExtension) { + fileType = utType.identifier + } else { + fileType = pathExtension + } + } else { + if let utType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil) { + fileType = utType.takeRetainedValue() as String + } else { + fileType = pathExtension + } + } + + let image = UIImage(data: data) + + DispatchQueue.main.async { + self.textStorages.forEach { value in + value.rangesForAttachment(attachment: self)?.forEach({ range in + value.textStorage?.deleteCharacters(in: range) + value.textStorage?.insert(NSAttributedString(attachment: ZNSTextAttachment(imageSize: image?.size, data: data, type: fileType)), at: range.location) + }) + } + self.labels.forEach { value in + value.rangesForAttachment(attachment: self)?.forEach({ range in + let attributedText = NSMutableAttributedString(attributedString: value.label?.attributedText ?? NSAttributedString()) + attributedText.deleteCharacters(in: range) + attributedText.insert(NSAttributedString(attachment: ZNSTextAttachment(imageSize: image?.size, data: data, type: fileType)), at: range.location) + value.label?.attributedText = attributedText + }) + } + self.delegate?.zNSTextAttachment(didLoad: self) + } + + self.isLoading = false + }) + + if let image = self.image { + return image + } + + return nil + } + + + public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + + if let image = self.image { + return CGRect(origin: origin ?? .zero, size: image.size) + } + + return .zero + } +} + +private extension ZNSTextAttachmentPlaceholder { + class WeakNSTextStorage { + + weak var textStorage: NSTextStorage? + + init(_ textStorage: NSTextStorage?) { + self.textStorage = textStorage + } + + func rangesForAttachment(attachment: ZNSTextAttachmentPlaceholder) -> [NSRange]? { + guard let attributedString = textStorage else { + return nil + } + let range = NSRange(location: 0, length: attributedString.string.utf16.count) + + var ranges = [NSRange]() + attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in + guard (value as? ZNSTextAttachmentPlaceholder) == attachment else { + return + } + ranges.append(effectiveRange) + } + + return (ranges.count == 0) ? (nil) : (ranges) + } + } + + func appendToTextStorages(with textStorage: NSTextStorage?) { + guard let textStorage = textStorage else { return } + if textStorages.contains(where: { value in + return value.textStorage === textStorage + }) { + return + } + + textStorages.append(WeakNSTextStorage(textStorage)) + } +} + +private extension ZNSTextAttachmentPlaceholder { + class WeakUILabel { + + weak var label: UILabel? + + init(_ label: UILabel?) { + self.label = label + } + + func rangesForAttachment(attachment: ZNSTextAttachmentPlaceholder) -> [NSRange]? { + guard let attributedString = label?.attributedText else { + return nil + } + let range = NSRange(location: 0, length: attributedString.string.utf16.count) + + var ranges = [NSRange]() + attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in + guard (value as? ZNSTextAttachmentPlaceholder) == attachment else { + return + } + ranges.append(effectiveRange) + } + + return (ranges.count == 0) ? (nil) : (ranges) + } + } + + func appendToLabels(with label: UILabel?) { + guard let label = label else { return } + if labels.contains(where: { value in + return value.label === label + }) { + return + } + + labels.append(WeakUILabel(label)) + } +} +#endif diff --git a/Sources/ZNSTextAttachment/ZNSTextAttachmentForMacOS.swift b/Sources/ZNSTextAttachment/ZNSTextAttachmentForMacOS.swift new file mode 100644 index 0000000..3575dfd --- /dev/null +++ b/Sources/ZNSTextAttachment/ZNSTextAttachmentForMacOS.swift @@ -0,0 +1,178 @@ +// +// ZNSTextAttachmentForMacOS.swift +// +// +// Created by zhgchgli on 2023/3/5. +// + +import Foundation +#if canImport(AppKit) +import AppKit +import UniformTypeIdentifiers + +public protocol ZNSTextAttachmentDataSource: AnyObject { + func zNSTextAttachment(_ textAttachment: ZNSTextAttachmentPlaceholder, loadImageURL imageURL: URL, completion: @escaping (Data) -> Void) +} + +public protocol ZNSTextAttachmentDelegate: AnyObject { + func zNSTextAttachment(didLoad textAttachment: ZNSTextAttachmentPlaceholder) +} + +public class ZNSTextAttachment: NSTextAttachment { + + public let imageSize: CGSize? + + public init(imageSize: CGSize?, data: Data, type: String) { + self.imageSize = imageSize + + super.init(data: data, ofType: type) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + + guard let imageWidth = imageSize?.width, + let imageHeight = imageSize?.height else { + return .zero + } + + let maxWidth = lineFrag.size.width - ((textContainer?.lineFragmentPadding ?? 0) * 2) + let factor = maxWidth / imageWidth + + return CGRect(origin: CGPoint.zero, size:CGSize(width: Int(imageWidth * factor), height: Int(imageHeight * factor))) + } +} + +public class ZNSTextAttachmentPlaceholder: NSTextAttachment { + + public let imageURL: URL + public weak var delegate: ZNSTextAttachmentDelegate? + public weak var dataSource: ZNSTextAttachmentDataSource? + + private let origin: CGPoint? + + private var isLoading: Bool = false + private var textStorages: [WeakNSTextStorage] = [] + + public init(imageURL: URL, placeholderImage: NSImage? = nil, placeholderImageOrigin: CGPoint? = nil) { + self.imageURL = imageURL + self.origin = placeholderImageOrigin + + if let placeholderImageData = placeholderImage?.tiffRepresentation { + super.init(data: placeholderImageData, ofType: "public.png") + } else { + super.init(data: nil, ofType: nil) + } + + self.image = placeholderImage + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func register(textStorage: NSTextStorage) { + self.appendToTextStorages(with: textStorage) + } + + public override func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> NSImage? { + + if let textStorage = textContainer?.layoutManager?.textStorage { + appendToTextStorages(with: textStorage) + } + + guard !isLoading else { return image } + isLoading = true + + dataSource?.zNSTextAttachment(self, loadImageURL: imageURL, completion: { data in + let fileType: String + let pathExtension = self.imageURL.pathExtension + if #available(macOS 11.0, *) { + if let utType = UTType(filenameExtension: pathExtension) { + fileType = utType.identifier + } else { + fileType = pathExtension + } + } else { + if let utType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil) { + fileType = utType.takeRetainedValue() as String + } else { + fileType = pathExtension + } + } + + let image = NSImage(data: data) + + DispatchQueue.main.async { + self.textStorages.forEach { value in + value.rangesForAttachment(attachment: self)?.forEach({ range in + value.textStorage?.deleteCharacters(in: range) + value.textStorage?.insert(NSAttributedString(attachment: ZNSTextAttachment(imageSize: image?.size, data: data, type: fileType)), at: range.location) + }) + } + self.delegate?.zNSTextAttachment(didLoad: self) + } + + self.isLoading = false + }) + + if let image = self.image { + return image + } + + return nil + } + + + public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + + if let image = self.image { + return CGRect(origin: origin ?? .zero, size: image.size) + } + + return .zero + } +} + +private extension ZNSTextAttachmentPlaceholder { + class WeakNSTextStorage { + + weak var textStorage: NSTextStorage? + + init(_ textStorage: NSTextStorage?) { + self.textStorage = textStorage + } + + func rangesForAttachment(attachment: ZNSTextAttachmentPlaceholder) -> [NSRange]? { + guard let attributedString = textStorage else { + return nil + } + let range = NSRange(location: 0, length: attributedString.string.utf16.count) + + var ranges = [NSRange]() + attributedString.enumerateAttribute(NSAttributedString.Key.attachment, in: range, options: []) { (value, effectiveRange, nil) in + guard (value as? ZNSTextAttachmentPlaceholder) == attachment else { + return + } + ranges.append(effectiveRange) + } + + return (ranges.count == 0) ? (nil) : (ranges) + } + } + + func appendToTextStorages(with textStorage: NSTextStorage?) { + guard let textStorage = textStorage else { return } + if textStorages.contains(where: { value in + return value.textStorage === textStorage + }) { + return + } + + textStorages.append(WeakNSTextStorage(textStorage)) + } +} +#endif diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/project.pbxproj b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..cb7b537 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 1EE6627E29B4D5FA0096B299 /* MixedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE6627D29B4D5FA0096B299 /* MixedViewController.swift */; }; + 1EFE8DE429B4AD9E007E2D44 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE8DE329B4AD9E007E2D44 /* AppDelegate.swift */; }; + 1EFE8DE629B4AD9E007E2D44 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE8DE529B4AD9E007E2D44 /* SceneDelegate.swift */; }; + 1EFE8DE829B4AD9E007E2D44 /* UITextViewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE8DE729B4AD9E007E2D44 /* UITextViewsViewController.swift */; }; + 1EFE8DEB29B4AD9E007E2D44 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE8DE929B4AD9E007E2D44 /* Main.storyboard */; }; + 1EFE8DED29B4AD9E007E2D44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE8DEC29B4AD9E007E2D44 /* Assets.xcassets */; }; + 1EFE8DF029B4AD9E007E2D44 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE8DEE29B4AD9E007E2D44 /* LaunchScreen.storyboard */; }; + 1EFE8DF929B4AE73007E2D44 /* ZNSTextAttachment in Frameworks */ = {isa = PBXBuildFile; productRef = 1EFE8DF829B4AE73007E2D44 /* ZNSTextAttachment */; }; + 1EFE8DFB29B4B039007E2D44 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE8DFA29B4B039007E2D44 /* TestData.swift */; }; + 1EFE8DFD29B4CD75007E2D44 /* UILabelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE8DFC29B4CD75007E2D44 /* UILabelsViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1EE6627D29B4D5FA0096B299 /* MixedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixedViewController.swift; sourceTree = ""; }; + 1EFE8DE029B4AD9E007E2D44 /* ZNSTextAttachment-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ZNSTextAttachment-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1EFE8DE329B4AD9E007E2D44 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1EFE8DE529B4AD9E007E2D44 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 1EFE8DE729B4AD9E007E2D44 /* UITextViewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewsViewController.swift; sourceTree = ""; }; + 1EFE8DEA29B4AD9E007E2D44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1EFE8DEC29B4AD9E007E2D44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1EFE8DEF29B4AD9E007E2D44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1EFE8DF129B4AD9E007E2D44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EFE8DFA29B4B039007E2D44 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; + 1EFE8DFC29B4CD75007E2D44 /* UILabelsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelsViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1EFE8DDD29B4AD9E007E2D44 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1EFE8DF929B4AE73007E2D44 /* ZNSTextAttachment in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1EFE8DD729B4AD9E007E2D44 = { + isa = PBXGroup; + children = ( + 1EFE8DE229B4AD9E007E2D44 /* ZNSTextAttachment-Demo */, + 1EFE8DE129B4AD9E007E2D44 /* Products */, + 1EFE8DF729B4AE73007E2D44 /* Frameworks */, + ); + sourceTree = ""; + }; + 1EFE8DE129B4AD9E007E2D44 /* Products */ = { + isa = PBXGroup; + children = ( + 1EFE8DE029B4AD9E007E2D44 /* ZNSTextAttachment-Demo.app */, + ); + name = Products; + sourceTree = ""; + }; + 1EFE8DE229B4AD9E007E2D44 /* ZNSTextAttachment-Demo */ = { + isa = PBXGroup; + children = ( + 1EFE8DE329B4AD9E007E2D44 /* AppDelegate.swift */, + 1EFE8DE529B4AD9E007E2D44 /* SceneDelegate.swift */, + 1EFE8DE729B4AD9E007E2D44 /* UITextViewsViewController.swift */, + 1EFE8DFC29B4CD75007E2D44 /* UILabelsViewController.swift */, + 1EE6627D29B4D5FA0096B299 /* MixedViewController.swift */, + 1EFE8DFA29B4B039007E2D44 /* TestData.swift */, + 1EFE8DE929B4AD9E007E2D44 /* Main.storyboard */, + 1EFE8DEC29B4AD9E007E2D44 /* Assets.xcassets */, + 1EFE8DEE29B4AD9E007E2D44 /* LaunchScreen.storyboard */, + 1EFE8DF129B4AD9E007E2D44 /* Info.plist */, + ); + path = "ZNSTextAttachment-Demo"; + sourceTree = ""; + }; + 1EFE8DF729B4AE73007E2D44 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1EFE8DDF29B4AD9E007E2D44 /* ZNSTextAttachment-Demo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1EFE8DF429B4AD9E007E2D44 /* Build configuration list for PBXNativeTarget "ZNSTextAttachment-Demo" */; + buildPhases = ( + 1EFE8DDC29B4AD9E007E2D44 /* Sources */, + 1EFE8DDD29B4AD9E007E2D44 /* Frameworks */, + 1EFE8DDE29B4AD9E007E2D44 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ZNSTextAttachment-Demo"; + packageProductDependencies = ( + 1EFE8DF829B4AE73007E2D44 /* ZNSTextAttachment */, + ); + productName = "ZNSTextAttachment-Demo"; + productReference = 1EFE8DE029B4AD9E007E2D44 /* ZNSTextAttachment-Demo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1EFE8DD829B4AD9E007E2D44 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 1EFE8DDF29B4AD9E007E2D44 = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = 1EFE8DDB29B4AD9E007E2D44 /* Build configuration list for PBXProject "ZNSTextAttachment-Demo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1EFE8DD729B4AD9E007E2D44; + productRefGroup = 1EFE8DE129B4AD9E007E2D44 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1EFE8DDF29B4AD9E007E2D44 /* ZNSTextAttachment-Demo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1EFE8DDE29B4AD9E007E2D44 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1EFE8DF029B4AD9E007E2D44 /* LaunchScreen.storyboard in Resources */, + 1EFE8DED29B4AD9E007E2D44 /* Assets.xcassets in Resources */, + 1EFE8DEB29B4AD9E007E2D44 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1EFE8DDC29B4AD9E007E2D44 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1EFE8DE829B4AD9E007E2D44 /* UITextViewsViewController.swift in Sources */, + 1EFE8DFD29B4CD75007E2D44 /* UILabelsViewController.swift in Sources */, + 1EE6627E29B4D5FA0096B299 /* MixedViewController.swift in Sources */, + 1EFE8DE429B4AD9E007E2D44 /* AppDelegate.swift in Sources */, + 1EFE8DE629B4AD9E007E2D44 /* SceneDelegate.swift in Sources */, + 1EFE8DFB29B4B039007E2D44 /* TestData.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 1EFE8DE929B4AD9E007E2D44 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1EFE8DEA29B4AD9E007E2D44 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1EFE8DEE29B4AD9E007E2D44 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1EFE8DEF29B4AD9E007E2D44 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1EFE8DF229B4AD9E007E2D44 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1EFE8DF329B4AD9E007E2D44 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1EFE8DF529B4AD9E007E2D44 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4U9BQ9X87W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ZNSTextAttachment-Demo/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "li.zhgchg.ZNSTextAttachment-Demo"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1EFE8DF629B4AD9E007E2D44 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 4U9BQ9X87W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ZNSTextAttachment-Demo/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "li.zhgchg.ZNSTextAttachment-Demo"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1EFE8DDB29B4AD9E007E2D44 /* Build configuration list for PBXProject "ZNSTextAttachment-Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1EFE8DF229B4AD9E007E2D44 /* Debug */, + 1EFE8DF329B4AD9E007E2D44 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1EFE8DF429B4AD9E007E2D44 /* Build configuration list for PBXNativeTarget "ZNSTextAttachment-Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1EFE8DF529B4AD9E007E2D44 /* Debug */, + 1EFE8DF629B4AD9E007E2D44 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1EFE8DF829B4AE73007E2D44 /* ZNSTextAttachment */ = { + isa = XCSwiftPackageProductDependency; + productName = ZNSTextAttachment; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1EFE8DD829B4AD9E007E2D44 /* Project object */; +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/xcshareddata/xcschemes/ZNSTextAttachment-Demo.xcscheme b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/xcshareddata/xcschemes/ZNSTextAttachment-Demo.xcscheme new file mode 100644 index 0000000..a889ed7 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo.xcodeproj/xcshareddata/xcschemes/ZNSTextAttachment-Demo.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/AppDelegate.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/AppDelegate.swift new file mode 100644 index 0000000..3e9a975 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AccentColor.colorset/Contents.json b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/Contents.json b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/LaunchScreen.storyboard b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/Main.storyboard b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/Main.storyboard new file mode 100644 index 0000000..381600f --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Base.lproj/Main.storyboard @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Info.plist b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/MixedViewController.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/MixedViewController.swift new file mode 100644 index 0000000..9b8f88d --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/MixedViewController.swift @@ -0,0 +1,54 @@ +// +// MixedViewController.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import UIKit +import ZNSTextAttachment + +class MixedViewController: UIViewController { + + @IBOutlet weak var textView: UITextView! + @IBOutlet weak var label: ZNSTextAttachmentLabel! + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + let attachment = ZNSTextAttachmentPlaceholder(imageURL: URL(string: "https://zhgchg.li/assets/a5643de271e4/1*A0yXupXW9-F9ZWe4gp2ObA.jpeg")!, placeholderImage: UIImage(systemName: "viewfinder.circle.fill")?.withTintColor(.red, renderingMode: .alwaysOriginal)) + + let data = TestData.generate(with: attachment) + + attachment.dataSource = self + attachment.delegate = self + + textView.attributedText = data + label.attributedText = data + } +} + +extension MixedViewController: ZNSTextAttachmentDataSource { + func zNSTextAttachment(_ textAttachment: ZNSTextAttachmentPlaceholder, loadImageURL imageURL: URL, completion: @escaping (Data) -> Void) { + let dataTask = URLSession.shared.dataTask(with: URL(string: imageURL.absoluteString+"?q=\(UUID().uuidString)")!) { (data, response, error) in + + guard let data = data, error == nil else { + print(error?.localizedDescription as Any) + return + } + + completion(data) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + dataTask.resume() + } + } +} + + +extension MixedViewController: ZNSTextAttachmentDelegate { + func zNSTextAttachment(didLoad textAttachment: ZNSTextAttachmentPlaceholder) { + // + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/SceneDelegate.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/SceneDelegate.swift new file mode 100644 index 0000000..b2f44e3 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/TestData.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/TestData.swift new file mode 100644 index 0000000..15f9820 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/TestData.swift @@ -0,0 +1,24 @@ +// +// TestData.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import Foundation +import ZNSTextAttachment + +struct TestData { + static func generate(with attachment: ZNSTextAttachmentPlaceholder) -> NSAttributedString { + let attributedString = NSMutableAttributedString() + attributedString.append(NSAttributedString(string: "- 使用純 Swift 開發,透過 Regex 剖析出 HTML Tag 並經過 Tokenization,分析修正 Tag 正確性(修正沒有 end 的 tag & 錯位 tag),再轉換成 abstract syntax tree,最終使用 Visitor Pattern 將 HTML Tag 與抽象樣式對應,得到最終 NSAttributedString 結果;其中不依賴任何 Parser Lib。\n")) + attributedString.append(NSAttributedString(string: "- 支援 HTML Render (to NSAttributedString) / Stripper (剝離 HTML Tag) / Selector 功能\n")) + + attributedString.append(NSAttributedString(attachment: attachment)) + + attributedString.append(NSAttributedString(string: "- 自動分析修正 Tag 正確性(修正沒有 end 的 tag & 錯位 tag)
->
BoldBold+ItalicItalic -> BoldBold+ItalicItalic -> (treat as String)\n")) + attributedString.append(NSAttributedString(string: "- 支援客製化樣式指定 e.g. -> weight: .semilbold & underline: 1\n")) + + return attributedString + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UILabelsViewController.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UILabelsViewController.swift new file mode 100644 index 0000000..8718061 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UILabelsViewController.swift @@ -0,0 +1,59 @@ +// +// UILabelsViewController.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import UIKit +import ZNSTextAttachment + +class UILabelsViewController: UIViewController { + + + @IBOutlet weak var label1: UILabel! + @IBOutlet weak var label2: UILabel! + + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + let attachment = ZNSTextAttachmentPlaceholder(imageURL: URL(string: "https://zhgchg.li/assets/a5643de271e4/1*A0yXupXW9-F9ZWe4gp2ObA.jpeg")!, placeholderImage: UIImage(systemName: "viewfinder.circle.fill")?.withTintColor(.red, renderingMode: .alwaysOriginal)) + + let data = TestData.generate(with: attachment) + + attachment.dataSource = self + attachment.delegate = self + + attachment.register(label: label1) + attachment.register(label: label2) + + label1.attributedText = data + label2.attributedText = data + } +} + +extension UILabelsViewController: ZNSTextAttachmentDataSource { + func zNSTextAttachment(_ textAttachment: ZNSTextAttachmentPlaceholder, loadImageURL imageURL: URL, completion: @escaping (Data) -> Void) { + let dataTask = URLSession.shared.dataTask(with: URL(string: imageURL.absoluteString+"?q=\(UUID().uuidString)")!) { (data, response, error) in + + guard let data = data, error == nil else { + print(error?.localizedDescription as Any) + return + } + + completion(data) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + dataTask.resume() + } + } +} + + +extension UILabelsViewController: ZNSTextAttachmentDelegate { + func zNSTextAttachment(didLoad textAttachment: ZNSTextAttachmentPlaceholder) { + // + } +} diff --git a/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UITextViewsViewController.swift b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UITextViewsViewController.swift new file mode 100644 index 0000000..ba1c2b9 --- /dev/null +++ b/ZNSTextAttachment-Demo/ZNSTextAttachment-Demo/UITextViewsViewController.swift @@ -0,0 +1,58 @@ +// +// UITextViewsViewController.swift +// ZNSTextAttachment-Demo +// +// Created by https://zhgchg.li on 2023/3/5. +// + +import UIKit +import ZNSTextAttachment + +class UITextViewsViewController: UIViewController { + + + @IBOutlet weak var textView1: UITextView! + @IBOutlet weak var textView2: UITextView! + @IBOutlet weak var textView3: UITextView! + + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + let attachment = ZNSTextAttachmentPlaceholder(imageURL: URL(string: "https://zhgchg.li/assets/a5643de271e4/1*A0yXupXW9-F9ZWe4gp2ObA.jpeg")!, placeholderImage: UIImage(systemName: "viewfinder.circle.fill")?.withTintColor(.red, renderingMode: .alwaysOriginal)) + + let data = TestData.generate(with: attachment) + + attachment.dataSource = self + attachment.delegate = self + + textView1.attributedText = data + textView2.attributedText = data + textView3.attributedText = data + } +} + +extension UITextViewsViewController: ZNSTextAttachmentDataSource { + func zNSTextAttachment(_ textAttachment: ZNSTextAttachmentPlaceholder, loadImageURL imageURL: URL, completion: @escaping (Data) -> Void) { + let dataTask = URLSession.shared.dataTask(with: URL(string: imageURL.absoluteString+"?q=\(UUID().uuidString)")!) { (data, response, error) in + + guard let data = data, error == nil else { + print(error?.localizedDescription as Any) + return + } + + completion(data) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + dataTask.resume() + } + } +} + + +extension UITextViewsViewController: ZNSTextAttachmentDelegate { + func zNSTextAttachment(didLoad textAttachment: ZNSTextAttachmentPlaceholder) { + + } +} diff --git a/ZNSTextAttachment.xcworkspace/contents.xcworkspacedata b/ZNSTextAttachment.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..17f0ebc --- /dev/null +++ b/ZNSTextAttachment.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/ZNSTextAttachment.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ZNSTextAttachment.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ZNSTextAttachment.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/scripts/ZNSTextAttachment.podspec b/scripts/ZNSTextAttachment.podspec new file mode 100644 index 0000000..4b154df --- /dev/null +++ b/scripts/ZNSTextAttachment.podspec @@ -0,0 +1,13 @@ +Pod::Spec.new do |s| + s.name = "ZNSTextAttachment" + s.version = "1.0.0" + s.summary = "ZNSTextAttachment enables NSTextAttachment to download images from remote URLs, and supports both UITextView and UILabel for multiple attachments." + s.homepage = "https://github.com/ZhgChgLi/ZNSTextAttachment" + s.license = { :type => "MIT", :file => "LICENSE" } + s.author = { "ZhgChgLi" => "me@zhgchg.li" } + s.source = { :git => "https://github.com/ZhgChgLi/ZNSTextAttachment.git", :tag => "v" + s.version.to_s } + s.ios.deployment_target = "12.0" + s.osx.deployment_target = "12.0" + s.swift_version = "5.0" + s.source_files = ["Sources/**/*.swift"] +end