├── .gitignore ├── EXP.png ├── IBEditor.png ├── LICENSE ├── Label.png ├── OBJC ├── SFAttributedString │ ├── SFAttributedString.h │ └── SFAttributedString.m ├── SFAttributedStringDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── apple.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── SFAttributedStringDemo │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── star.imageset │ │ ├── Contents.json │ │ └── star@2x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.h │ ├── SceneDelegate.m │ ├── ViewController.h │ ├── ViewController.m │ └── main.m ├── README.md ├── SFAttributedString.podspec ├── SFAttributedStringSwift.podspec ├── SWIFT ├── Nothing │ └── SwiftyJSON.swift ├── SFAttributedString │ └── SFAttributedString.swift ├── SFAttributedStringSwiftDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── SFAttributedStringSwiftDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── star.imageset │ │ ├── Contents.json │ │ └── star@2x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift └── TEST_IMG.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | 29 | # CocoaPods 30 | # 31 | # We recommend against adding the Pods directory to your .gitignore. However 32 | # you should judge for yourself, the pros and cons are mentioned at: 33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 34 | # 35 | Pods/ 36 | 37 | # Carthage 38 | # 39 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 40 | # Carthage/Checkouts 41 | 42 | Carthage/Build 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/screenshots -------------------------------------------------------------------------------- /EXP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/EXP.png -------------------------------------------------------------------------------- /IBEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/IBEditor.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2025 AutoPropertyCocoa (https://github.com/Meterwhite/SFAttributedString) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/Label.png -------------------------------------------------------------------------------- /OBJC/SFAttributedString/SFAttributedString.h: -------------------------------------------------------------------------------- 1 | // 2 | // SFAttributedString.h 3 | // SFAttributedString 4 | // https://github.com/Meterwhite/SFAttributedString 5 | // 6 | // Created by MeterWhite on 2020/7/18. 7 | // Copyright © 2020 Meterwhite. All rights reserved. 8 | // 9 | 10 | #import 11 | #import 12 | 13 | #pragma mark - Category for NSString 14 | 15 | @interface NSString(SimpleFormatAttributedString) 16 | 17 | /// Core 18 | - (nonnull NSAttributedString *)sf_evalString; 19 | 20 | /// No text labels, no image labels 21 | - (nonnull NSString *)sf_unformattedString; 22 | 23 | @end 24 | 25 | #pragma mark - SFAtStringCore 26 | 27 | @interface SFAtStringCore :NSObject 28 | 29 | /// Register an attribute label 30 | /// @param lb Letters, numbers, underscores are allowed.(允许字母,数字,下划线。) 31 | + (void)registerAttributes:(nonnull NSDictionary *)adic forLabel:(nonnull NSString *)lb; 32 | 33 | @end 34 | 35 | #pragma mark - IB Inspectable supported(Support setting on XIB editor) 36 | @interface UITextView(SimpleFormatAttributedString) 37 | 38 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_text; 39 | 40 | @end 41 | 42 | @interface UITextField(SimpleFormatAttributedString) 43 | 44 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_text; 45 | 46 | @end 47 | 48 | @interface UIButton(SimpleFormatAttributedString) 49 | 50 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_title_default; 51 | 52 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_title_highlighted; 53 | 54 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_title_selected; 55 | 56 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_title_disabled; 57 | 58 | @end 59 | 60 | @interface UILabel(SimpleFormatAttributedString) 61 | 62 | @property (nullable,nonatomic,copy) IBInspectable NSString* sf_text; 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /OBJC/SFAttributedString/SFAttributedString.m: -------------------------------------------------------------------------------- 1 | // 2 | // SFAttributedString.m 3 | // SFAttributedString 4 | // https://github.com/Meterwhite/SFAttributedString 5 | // 6 | // Created by MeterWhite on 2020/7/18. 7 | // Copyright © 2020 Meterwhite. All rights reserved. 8 | // 9 | 10 | #import "SFAttributedString.h" 11 | @class SFImageLbAttachment; 12 | /// String : Dictionary 13 | static NSMutableDictionary *_cached_lb_adic; 14 | /// String : NSAttributedString 15 | static NSMutableDictionary *_cached_txt_ats; 16 | /// weak(View) : copy(String|Dictionary) 17 | static NSMapTable *_cached_v_sftxt; 18 | static NSRegularExpression *_rgx_txt; 19 | static NSRegularExpression *_rgx_img; 20 | 21 | /// Return type : NSArray* 22 | NSArray *combineCks(NSArray *cks_lb,NSArray *cks_img); 23 | /// 0 - Image 24 | /// 1 - Rect 25 | NS_INLINE void setAtmFromImgLb(SFImageLbAttachment *atm,NSString *lb); 26 | NSArray *atmsForImgLb(NSString *string, NSArray *cks_txt); 27 | 28 | @interface SFImageLbAttachment : NSTextAttachment 29 | @property (nonatomic) NSRange conRange; 30 | @property (nonatomic) NSInteger offset; 31 | @property (nonatomic,readonly) NSUInteger inserdex; 32 | @end 33 | 34 | @implementation SFImageLbAttachment 35 | 36 | - (NSUInteger)inserdex { 37 | return _conRange.location - _offset; 38 | } 39 | 40 | @end 41 | 42 | 43 | @implementation SFAtStringCore 44 | + (void)initialize { 45 | if(self != SFAtStringCore.class) return; 46 | _cached_lb_adic = [NSMutableDictionary dictionary]; 47 | _cached_txt_ats = [NSMutableDictionary dictionary]; 48 | _cached_v_sftxt = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPersonality) valueOptions:(NSPointerFunctionsCopyIn|NSPointerFunctionsObjectPersonality)]; 49 | _rgx_txt = [NSRegularExpression regularExpressionWithPattern:@"\\[\\w+\\]" options:0 error:nil]; 50 | _rgx_img = [NSRegularExpression regularExpressionWithPattern:@"\\[\\[!\\]\\w+(,(-|\\d|\\.)+)*\\]" options:0 error:nil]; 51 | } 52 | 53 | + (void)registerAttributes:(NSDictionary *)adic forLabel:(NSString *)lb { 54 | NSAssert(adic && lb, @"Nonnull!"); 55 | _cached_lb_adic[[NSString stringWithFormat:@"[%@]",lb]] = adic; 56 | } 57 | 58 | + (NSAttributedString *)evalScript:(NSString *)string { 59 | /// Cached first 60 | NSString *k = [string copy]; 61 | if(_cached_txt_ats[k]) { 62 | return _cached_txt_ats[k]; 63 | } 64 | /// Split image labels, suspend them.(剥离图片标签,挂起图片标签相关信息) 65 | NSArray *cks_txt = [_rgx_txt matchesInString:string options:0 range:NSMakeRange(0, string.length)]; 66 | NSArray *cks_img = [_rgx_img matchesInString:string options:0 range:NSMakeRange(0, string.length)]; 67 | NSArray *img_atms; 68 | if(cks_img.count) { 69 | img_atms = atmsForImgLb(string, cks_txt); 70 | NSMutableString *mstring = string.mutableCopy; 71 | for (NSTextCheckingResult *item in cks_img.reverseObjectEnumerator) { 72 | [mstring deleteCharactersInRange:item.range]; 73 | } 74 | string = mstring.copy; 75 | } 76 | /// Here begins to parse the plain text(这里开始解析纯文本) 77 | cks_txt = [_rgx_txt matchesInString:string options:0 range:NSMakeRange(0, string.length)]; 78 | /// Filter invalid labels 79 | cks_txt = [cks_txt filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSTextCheckingResult *rt, NSDictionary * _) { 80 | return (_cached_lb_adic[[string substringWithRange:rt.range]] != nil); 81 | }]]; 82 | NSMutableAttributedString * m_ret = [NSMutableAttributedString new]; 83 | NSRange txtRange; 84 | if(cks_txt.count == 0) { 85 | NSAssert(0, @"Missing attribute label!"); 86 | return nil; 87 | } 88 | if(cks_txt.firstObject.range.location != 0 || 89 | nil == _cached_lb_adic[[string substringWithRange:cks_txt.firstObject.range]]) { 90 | NSAssert(0, @"The first attribute label is missing!"); 91 | return nil; 92 | } 93 | for (NSInteger i = 0; i < cks_txt.count; i++) { 94 | NSTextCheckingResult *curr = cks_txt[i]; 95 | txtRange.location = curr.range.location + curr.range.length; 96 | if(i == cks_txt.count - 1) { 97 | if(txtRange.location >= string.length) { 98 | NSAssert(0, @"Missing text at the end of the string!"); 99 | return nil; 100 | } 101 | txtRange.length = string.length - txtRange.location; 102 | } else { 103 | NSTextCheckingResult *next = cks_txt[i+1]; 104 | txtRange.length = next.range.location - txtRange.location; 105 | } 106 | NSString *iTxt = [string substringWithRange:txtRange]; 107 | NSString *iLb = [string substringWithRange:curr.range]; 108 | id adic = _cached_lb_adic[iLb]; 109 | [m_ret appendAttributedString:[NSAttributedString.alloc initWithString:iTxt attributes:adic]]; 110 | } 111 | /// Append image labels 112 | if(img_atms.count) { 113 | for (SFImageLbAttachment *atm in img_atms.reverseObjectEnumerator) { 114 | [m_ret insertAttributedString:[NSAttributedString attributedStringWithAttachment:atm] atIndex:atm.inserdex]; 115 | } 116 | } 117 | _cached_txt_ats[k] = [m_ret copy]; 118 | return [m_ret copy]; 119 | } 120 | 121 | + (NSString *)unformatted:(NSString *)string { 122 | NSMutableString *ret = [string mutableCopy]; 123 | NSArray *cks_txt = [_rgx_txt matchesInString:string options:0 range:NSMakeRange(0, string.length)]; 124 | cks_txt = [cks_txt filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSTextCheckingResult *rt, NSDictionary * _) { 125 | return (_cached_lb_adic[[string substringWithRange:rt.range]] != nil); 126 | }]]; 127 | NSArray *cks_img = [_rgx_img matchesInString:string options:0 range:NSMakeRange(0, string.length)]; 128 | NSArray *cks = combineCks(cks_txt, cks_img); 129 | if(cks.count == 0) { 130 | NSAssert(0, @"Missing attribute label!"); 131 | return nil; 132 | } 133 | if(cks.firstObject.range.location != 0 || 134 | ([cks_txt containsObject:cks.firstObject] && nil == _cached_lb_adic[[string substringWithRange:cks.firstObject.range]])) { 135 | NSAssert(0, @"The first attribute label is missing!"); 136 | return nil; 137 | } 138 | for (NSTextCheckingResult *item in cks.reverseObjectEnumerator) { 139 | [ret deleteCharactersInRange:item.range]; 140 | } 141 | return [ret copy]; 142 | } 143 | @end 144 | 145 | @implementation NSString(SimpleFormatAttributedString) 146 | - (nonnull NSAttributedString *)sf_evalString { 147 | return [SFAtStringCore evalScript:self]; 148 | } 149 | 150 | - (NSString *)sf_unformattedString { 151 | return [SFAtStringCore unformatted:self]; 152 | } 153 | @end 154 | 155 | #pragma mark - Private extention/UIView 156 | @interface UIView (SimpleFormatAttributedString) 157 | @property (nullable,nonatomic,copy) NSString *sf_text; 158 | @end 159 | 160 | @implementation UIView (SimpleFormatAttributedString) 161 | 162 | - (void)setSf_text:(NSString *)sf_text { 163 | [_cached_v_sftxt setObject:sf_text forKey:self]; 164 | } 165 | 166 | - (NSString *)sf_text { 167 | return [_cached_v_sftxt objectForKey:self]; 168 | } 169 | 170 | @end 171 | 172 | #pragma mark - IB Inspectable supported 173 | 174 | @implementation UITextView (SimpleFormatAttributedString) 175 | 176 | - (void)setSf_text:(NSString *)sf_text { 177 | [super setSf_text:sf_text]; 178 | [self setAttributedText:sf_text.sf_evalString]; 179 | } 180 | 181 | @end 182 | 183 | @implementation UITextField (SimpleFormatAttributedString) 184 | 185 | - (void)setSf_text:(NSString *)sf_text { 186 | [super setSf_text:sf_text]; 187 | [self setAttributedText:sf_text.sf_evalString]; 188 | } 189 | 190 | @end 191 | 192 | @implementation UIButton (SimpleFormatAttributedString) 193 | - (NSString *)sf_title_default { 194 | id userInfo = [_cached_v_sftxt objectForKey:self]; 195 | return userInfo[@(UIControlStateNormal)]; 196 | } 197 | 198 | - (void)setSf_title_default:(NSString *)tt { 199 | id userInfo = [_cached_v_sftxt objectForKey:self]; 200 | userInfo = userInfo ? [userInfo mutableCopy] : [NSMutableDictionary dictionary]; 201 | userInfo[@(UIControlStateNormal)] = tt; 202 | [_cached_v_sftxt setObject:userInfo forKey:self]; 203 | [self setAttributedTitle:tt.sf_evalString forState:UIControlStateNormal]; 204 | } 205 | 206 | - (NSString *)sf_title_highlighted { 207 | id userInfo = [_cached_v_sftxt objectForKey:self]; 208 | return userInfo[@(UIControlStateHighlighted)]; 209 | } 210 | 211 | - (void)setSf_title_highlighted:(NSString *)tt { 212 | id userInfo = [_cached_v_sftxt objectForKey:self]; 213 | userInfo = userInfo ? [userInfo mutableCopy] : [NSMutableDictionary dictionary]; 214 | userInfo[@(UIControlStateHighlighted)] = tt; 215 | [_cached_v_sftxt setObject:userInfo forKey:self]; 216 | [self setAttributedTitle:tt.sf_evalString forState:UIControlStateHighlighted]; 217 | } 218 | 219 | - (NSString *)sf_title_selected { 220 | id userInfo = [_cached_v_sftxt objectForKey:self]; 221 | return userInfo[@(UIControlStateSelected)]; 222 | } 223 | 224 | - (void)setSf_title_selected:(NSString *)tt { 225 | id userInfo = [_cached_v_sftxt objectForKey:self]; 226 | userInfo = userInfo ? [userInfo mutableCopy] : [NSMutableDictionary dictionary]; 227 | userInfo[@(UIControlStateSelected)] = tt; 228 | [_cached_v_sftxt setObject:userInfo forKey:self]; 229 | [self setAttributedTitle:tt.sf_evalString forState:UIControlStateSelected]; 230 | } 231 | 232 | - (NSString *)sf_title_disabled { 233 | id userInfo = [_cached_v_sftxt objectForKey:self]; 234 | return userInfo[@(UIControlStateDisabled)]; 235 | } 236 | 237 | - (void)setSf_title_disabled:(NSString *)tt { 238 | id userInfo = [_cached_v_sftxt objectForKey:self]; 239 | userInfo = userInfo ? [userInfo mutableCopy] : [NSMutableDictionary dictionary]; 240 | userInfo[@(UIControlStateDisabled)] = tt; 241 | [_cached_v_sftxt setObject:userInfo forKey:self]; 242 | [self setAttributedTitle:tt.sf_evalString forState:UIControlStateDisabled]; 243 | } 244 | @end 245 | 246 | @implementation UILabel (SimpleFormatAttributedString) 247 | 248 | - (void)setSf_text:(NSString *)sf_text { 249 | [super setSf_text:sf_text]; 250 | [self setAttributedText:sf_text.sf_evalString]; 251 | } 252 | 253 | @end 254 | 255 | NSArray *combineCks(NSArray *cks_txt,NSArray *cks_img) { 256 | if(0 == cks_txt.count) return cks_img; 257 | if(0 == cks_img.count) return cks_txt; 258 | NSMutableArray *a = [NSMutableArray array]; 259 | [a addObjectsFromArray:cks_txt]; 260 | [a addObjectsFromArray:cks_img]; 261 | [a sortUsingComparator:^NSComparisonResult(NSTextCheckingResult* obj1, NSTextCheckingResult* obj2) { 262 | if(obj1.range.location < obj2.range.location) { 263 | return NSOrderedAscending; 264 | } else if (obj1.range.location > obj2.range.location) { 265 | return NSOrderedDescending; 266 | } 267 | assert(0);/// If bug 268 | }]; 269 | return a.copy; 270 | } 271 | 272 | NS_INLINE void setAtmFromImgLb(SFImageLbAttachment *atm,NSString *lb) { 273 | NSArray *cpts = [[lb substringWithRange:(NSMakeRange(4, lb.length - 5))] componentsSeparatedByString:@","]; 274 | atm.image = [UIImage imageNamed:cpts[0]]; 275 | if(cpts.count == 1) return; 276 | if(cpts.count != 5) { 277 | assert(0);/// Invalid image label format. 278 | return; 279 | } 280 | NSDecimalNumber *x_nb = [NSDecimalNumber decimalNumberWithString:cpts[1]]; 281 | NSDecimalNumber *y_nb = [NSDecimalNumber decimalNumberWithString:cpts[2]]; 282 | NSDecimalNumber *w_nb = [NSDecimalNumber decimalNumberWithString:cpts[3]]; 283 | NSDecimalNumber *h_nb = [NSDecimalNumber decimalNumberWithString:cpts[4]]; 284 | atm.bounds = CGRectMake(x_nb.floatValue, y_nb.floatValue, w_nb.floatValue, h_nb.floatValue); 285 | } 286 | 287 | NSArray * atmsForImgLb(NSString *string, NSArray *cks_txt) { 288 | NSMutableArray *m_ret = [NSMutableArray array]; 289 | NSInteger offset = 0; 290 | NSMutableString *nonTxtLbString = string.mutableCopy; 291 | for (NSTextCheckingResult *item in cks_txt.reverseObjectEnumerator) { 292 | if(_cached_lb_adic[[nonTxtLbString substringWithRange:item.range]]){ 293 | [nonTxtLbString deleteCharactersInRange:item.range]; 294 | } 295 | } 296 | NSArray *cks_img = [_rgx_img matchesInString:nonTxtLbString options:0 range:NSMakeRange(0, [nonTxtLbString length])]; 297 | for (NSUInteger i = 0; i < cks_img.count; i++) { 298 | NSTextCheckingResult *ck = cks_img[i]; 299 | SFImageLbAttachment *atm = [SFImageLbAttachment new]; 300 | setAtmFromImgLb(atm,[nonTxtLbString substringWithRange:ck.range]); 301 | atm.conRange = ck.range; 302 | atm.offset = offset; 303 | offset += (atm.conRange.length); 304 | [m_ret addObject:atm]; 305 | } 306 | return m_ret.copy; 307 | } 308 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 18B000D824C201E2004B6A59 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 18B000D724C201E2004B6A59 /* AppDelegate.m */; }; 11 | 18B000DB24C201E2004B6A59 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 18B000DA24C201E2004B6A59 /* SceneDelegate.m */; }; 12 | 18B000DE24C201E2004B6A59 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 18B000DD24C201E2004B6A59 /* ViewController.m */; }; 13 | 18B000E124C201E2004B6A59 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 18B000DF24C201E2004B6A59 /* Main.storyboard */; }; 14 | 18B000E324C201E4004B6A59 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18B000E224C201E4004B6A59 /* Assets.xcassets */; }; 15 | 18B000E624C201E4004B6A59 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 18B000E424C201E4004B6A59 /* LaunchScreen.storyboard */; }; 16 | 18B000E924C201E4004B6A59 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 18B000E824C201E4004B6A59 /* main.m */; }; 17 | 18F301DA24C2A77700F41F89 /* SFAttributedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 18F301D924C2A77700F41F89 /* SFAttributedString.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 18B000D324C201E2004B6A59 /* SFAttributedStringDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SFAttributedStringDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 18B000D624C201E2004B6A59 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 23 | 18B000D724C201E2004B6A59 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 24 | 18B000D924C201E2004B6A59 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 25 | 18B000DA24C201E2004B6A59 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 26 | 18B000DC24C201E2004B6A59 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 27 | 18B000DD24C201E2004B6A59 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 28 | 18B000E024C201E2004B6A59 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 29 | 18B000E224C201E4004B6A59 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 18B000E524C201E4004B6A59 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | 18B000E724C201E4004B6A59 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 18B000E824C201E4004B6A59 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 33 | 18F301D824C2A77700F41F89 /* SFAttributedString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFAttributedString.h; sourceTree = ""; }; 34 | 18F301D924C2A77700F41F89 /* SFAttributedString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFAttributedString.m; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 18B000D024C201E2004B6A59 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 18B000CA24C201E2004B6A59 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 18B000D524C201E2004B6A59 /* SFAttributedStringDemo */, 52 | 18B000D424C201E2004B6A59 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 18B000D424C201E2004B6A59 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 18B000D324C201E2004B6A59 /* SFAttributedStringDemo.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 18B000D524C201E2004B6A59 /* SFAttributedStringDemo */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 18B000EF24C20259004B6A59 /* SFAttributedString */, 68 | 18B000D624C201E2004B6A59 /* AppDelegate.h */, 69 | 18B000D724C201E2004B6A59 /* AppDelegate.m */, 70 | 18B000D924C201E2004B6A59 /* SceneDelegate.h */, 71 | 18B000DA24C201E2004B6A59 /* SceneDelegate.m */, 72 | 18B000DC24C201E2004B6A59 /* ViewController.h */, 73 | 18B000DD24C201E2004B6A59 /* ViewController.m */, 74 | 18B000DF24C201E2004B6A59 /* Main.storyboard */, 75 | 18B000E224C201E4004B6A59 /* Assets.xcassets */, 76 | 18B000E424C201E4004B6A59 /* LaunchScreen.storyboard */, 77 | 18B000E724C201E4004B6A59 /* Info.plist */, 78 | 18B000E824C201E4004B6A59 /* main.m */, 79 | ); 80 | path = SFAttributedStringDemo; 81 | sourceTree = ""; 82 | }; 83 | 18B000EF24C20259004B6A59 /* SFAttributedString */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 18F301D824C2A77700F41F89 /* SFAttributedString.h */, 87 | 18F301D924C2A77700F41F89 /* SFAttributedString.m */, 88 | ); 89 | path = SFAttributedString; 90 | sourceTree = SOURCE_ROOT; 91 | }; 92 | /* End PBXGroup section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | 18B000D224C201E2004B6A59 /* SFAttributedStringDemo */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = 18B000EC24C201E4004B6A59 /* Build configuration list for PBXNativeTarget "SFAttributedStringDemo" */; 98 | buildPhases = ( 99 | 18B000CF24C201E2004B6A59 /* Sources */, 100 | 18B000D024C201E2004B6A59 /* Frameworks */, 101 | 18B000D124C201E2004B6A59 /* Resources */, 102 | ); 103 | buildRules = ( 104 | ); 105 | dependencies = ( 106 | ); 107 | name = SFAttributedStringDemo; 108 | productName = SFAttributedStringDemo; 109 | productReference = 18B000D324C201E2004B6A59 /* SFAttributedStringDemo.app */; 110 | productType = "com.apple.product-type.application"; 111 | }; 112 | /* End PBXNativeTarget section */ 113 | 114 | /* Begin PBXProject section */ 115 | 18B000CB24C201E2004B6A59 /* Project object */ = { 116 | isa = PBXProject; 117 | attributes = { 118 | LastUpgradeCheck = 1130; 119 | ORGANIZATIONNAME = Meterwhite; 120 | TargetAttributes = { 121 | 18B000D224C201E2004B6A59 = { 122 | CreatedOnToolsVersion = 11.3.1; 123 | }; 124 | }; 125 | }; 126 | buildConfigurationList = 18B000CE24C201E2004B6A59 /* Build configuration list for PBXProject "SFAttributedStringDemo" */; 127 | compatibilityVersion = "Xcode 9.3"; 128 | developmentRegion = en; 129 | hasScannedForEncodings = 0; 130 | knownRegions = ( 131 | en, 132 | Base, 133 | ); 134 | mainGroup = 18B000CA24C201E2004B6A59; 135 | productRefGroup = 18B000D424C201E2004B6A59 /* Products */; 136 | projectDirPath = ""; 137 | projectRoot = ""; 138 | targets = ( 139 | 18B000D224C201E2004B6A59 /* SFAttributedStringDemo */, 140 | ); 141 | }; 142 | /* End PBXProject section */ 143 | 144 | /* Begin PBXResourcesBuildPhase section */ 145 | 18B000D124C201E2004B6A59 /* Resources */ = { 146 | isa = PBXResourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 18B000E624C201E4004B6A59 /* LaunchScreen.storyboard in Resources */, 150 | 18B000E324C201E4004B6A59 /* Assets.xcassets in Resources */, 151 | 18B000E124C201E2004B6A59 /* Main.storyboard in Resources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXResourcesBuildPhase section */ 156 | 157 | /* Begin PBXSourcesBuildPhase section */ 158 | 18B000CF24C201E2004B6A59 /* Sources */ = { 159 | isa = PBXSourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 18B000DE24C201E2004B6A59 /* ViewController.m in Sources */, 163 | 18B000D824C201E2004B6A59 /* AppDelegate.m in Sources */, 164 | 18F301DA24C2A77700F41F89 /* SFAttributedString.m in Sources */, 165 | 18B000E924C201E4004B6A59 /* main.m in Sources */, 166 | 18B000DB24C201E2004B6A59 /* SceneDelegate.m in Sources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXSourcesBuildPhase section */ 171 | 172 | /* Begin PBXVariantGroup section */ 173 | 18B000DF24C201E2004B6A59 /* Main.storyboard */ = { 174 | isa = PBXVariantGroup; 175 | children = ( 176 | 18B000E024C201E2004B6A59 /* Base */, 177 | ); 178 | name = Main.storyboard; 179 | sourceTree = ""; 180 | }; 181 | 18B000E424C201E4004B6A59 /* LaunchScreen.storyboard */ = { 182 | isa = PBXVariantGroup; 183 | children = ( 184 | 18B000E524C201E4004B6A59 /* Base */, 185 | ); 186 | name = LaunchScreen.storyboard; 187 | sourceTree = ""; 188 | }; 189 | /* End PBXVariantGroup section */ 190 | 191 | /* Begin XCBuildConfiguration section */ 192 | 18B000EA24C201E4004B6A59 /* Debug */ = { 193 | isa = XCBuildConfiguration; 194 | buildSettings = { 195 | ALWAYS_SEARCH_USER_PATHS = NO; 196 | CLANG_ANALYZER_NONNULL = YES; 197 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 199 | CLANG_CXX_LIBRARY = "libc++"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_ENABLE_OBJC_WEAK = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 | CLANG_WARN_EMPTY_BODY = YES; 211 | CLANG_WARN_ENUM_CONVERSION = YES; 212 | CLANG_WARN_INFINITE_RECURSION = YES; 213 | CLANG_WARN_INT_CONVERSION = YES; 214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | COPY_PHASE_STRIP = NO; 225 | DEBUG_INFORMATION_FORMAT = dwarf; 226 | ENABLE_STRICT_OBJC_MSGSEND = YES; 227 | ENABLE_TESTABILITY = YES; 228 | GCC_C_LANGUAGE_STANDARD = gnu11; 229 | GCC_DYNAMIC_NO_PIC = NO; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_OPTIMIZATION_LEVEL = 0; 232 | GCC_PREPROCESSOR_DEFINITIONS = ( 233 | "DEBUG=1", 234 | "$(inherited)", 235 | ); 236 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 237 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 238 | GCC_WARN_UNDECLARED_SELECTOR = YES; 239 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 240 | GCC_WARN_UNUSED_FUNCTION = YES; 241 | GCC_WARN_UNUSED_VARIABLE = YES; 242 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 243 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 244 | MTL_FAST_MATH = YES; 245 | ONLY_ACTIVE_ARCH = YES; 246 | SDKROOT = iphoneos; 247 | }; 248 | name = Debug; 249 | }; 250 | 18B000EB24C201E4004B6A59 /* Release */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | ALWAYS_SEARCH_USER_PATHS = NO; 254 | CLANG_ANALYZER_NONNULL = YES; 255 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_ENABLE_OBJC_WEAK = YES; 261 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 262 | CLANG_WARN_BOOL_CONVERSION = YES; 263 | CLANG_WARN_COMMA = YES; 264 | CLANG_WARN_CONSTANT_CONVERSION = YES; 265 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 266 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 273 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 274 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 277 | CLANG_WARN_STRICT_PROTOTYPES = YES; 278 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 279 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 280 | CLANG_WARN_UNREACHABLE_CODE = YES; 281 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 282 | COPY_PHASE_STRIP = NO; 283 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 284 | ENABLE_NS_ASSERTIONS = NO; 285 | ENABLE_STRICT_OBJC_MSGSEND = YES; 286 | GCC_C_LANGUAGE_STANDARD = gnu11; 287 | GCC_NO_COMMON_BLOCKS = YES; 288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 290 | GCC_WARN_UNDECLARED_SELECTOR = YES; 291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 292 | GCC_WARN_UNUSED_FUNCTION = YES; 293 | GCC_WARN_UNUSED_VARIABLE = YES; 294 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 295 | MTL_ENABLE_DEBUG_INFO = NO; 296 | MTL_FAST_MATH = YES; 297 | SDKROOT = iphoneos; 298 | VALIDATE_PRODUCT = YES; 299 | }; 300 | name = Release; 301 | }; 302 | 18B000ED24C201E4004B6A59 /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | CODE_SIGN_STYLE = Automatic; 307 | INFOPLIST_FILE = SFAttributedStringDemo/Info.plist; 308 | LD_RUNPATH_SEARCH_PATHS = ( 309 | "$(inherited)", 310 | "@executable_path/Frameworks", 311 | ); 312 | PRODUCT_BUNDLE_IDENTIFIER = com.git.meterwhite.SFAttributedStringDemo; 313 | PRODUCT_NAME = "$(TARGET_NAME)"; 314 | TARGETED_DEVICE_FAMILY = "1,2"; 315 | }; 316 | name = Debug; 317 | }; 318 | 18B000EE24C201E4004B6A59 /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 322 | CODE_SIGN_STYLE = Automatic; 323 | INFOPLIST_FILE = SFAttributedStringDemo/Info.plist; 324 | LD_RUNPATH_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "@executable_path/Frameworks", 327 | ); 328 | PRODUCT_BUNDLE_IDENTIFIER = com.git.meterwhite.SFAttributedStringDemo; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | }; 332 | name = Release; 333 | }; 334 | /* End XCBuildConfiguration section */ 335 | 336 | /* Begin XCConfigurationList section */ 337 | 18B000CE24C201E2004B6A59 /* Build configuration list for PBXProject "SFAttributedStringDemo" */ = { 338 | isa = XCConfigurationList; 339 | buildConfigurations = ( 340 | 18B000EA24C201E4004B6A59 /* Debug */, 341 | 18B000EB24C201E4004B6A59 /* Release */, 342 | ); 343 | defaultConfigurationIsVisible = 0; 344 | defaultConfigurationName = Release; 345 | }; 346 | 18B000EC24C201E4004B6A59 /* Build configuration list for PBXNativeTarget "SFAttributedStringDemo" */ = { 347 | isa = XCConfigurationList; 348 | buildConfigurations = ( 349 | 18B000ED24C201E4004B6A59 /* Debug */, 350 | 18B000EE24C201E4004B6A59 /* Release */, 351 | ); 352 | defaultConfigurationIsVisible = 0; 353 | defaultConfigurationName = Release; 354 | }; 355 | /* End XCConfigurationList section */ 356 | }; 357 | rootObject = 18B000CB24C201E2004B6A59 /* Project object */; 358 | } 359 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo.xcodeproj/xcuserdata/apple.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo.xcodeproj/xcuserdata/apple.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SFAttributedStringDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | #pragma mark - UISceneSession lifecycle 25 | 26 | 27 | - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; 31 | } 32 | 33 | 34 | - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { 35 | // Called when the user discards a scene session. 36 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 37 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 38 | } 39 | 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Assets.xcassets/star.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "star@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Assets.xcassets/star.imageset/star@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/OBJC/SFAttributedStringDemo/Assets.xcassets/star.imageset/star@2x.png -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 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. 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/SceneDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.h 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SceneDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow * window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/SceneDelegate.m: -------------------------------------------------------------------------------- 1 | #import "SceneDelegate.h" 2 | 3 | @interface SceneDelegate () 4 | 5 | @end 6 | 7 | @implementation SceneDelegate 8 | 9 | 10 | - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { 11 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 12 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 13 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 14 | } 15 | 16 | 17 | - (void)sceneDidDisconnect:(UIScene *)scene { 18 | // Called as the scene is being released by the system. 19 | // This occurs shortly after the scene enters the background, or when its session is discarded. 20 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 21 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 22 | } 23 | 24 | 25 | - (void)sceneDidBecomeActive:(UIScene *)scene { 26 | // Called when the scene has moved from an inactive state to an active state. 27 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 28 | } 29 | 30 | 31 | - (void)sceneWillResignActive:(UIScene *)scene { 32 | // Called when the scene will move from an active state to an inactive state. 33 | // This may occur due to temporary interruptions (ex. an incoming phone call). 34 | } 35 | 36 | 37 | - (void)sceneWillEnterForeground:(UIScene *)scene { 38 | // Called as the scene transitions from the background to the foreground. 39 | // Use this method to undo the changes made on entering the background. 40 | } 41 | 42 | 43 | - (void)sceneDidEnterBackground:(UIScene *)scene { 44 | // Called as the scene transitions from the foreground to the background. 45 | // Use this method to save data, release shared resources, and store enough scene-specific state information 46 | // to restore the scene back to its current state. 47 | } 48 | 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "SFAttributedString.h" 11 | 12 | @interface ViewController () 13 | @property (unsafe_unretained, nonatomic) IBOutlet UILabel *lb; 14 | @property (unsafe_unretained, nonatomic) IBOutlet UIButton *bt; 15 | @property (unsafe_unretained, nonatomic) IBOutlet UITextView *txtv; 16 | @property (unsafe_unretained, nonatomic) IBOutlet UITextField *txtfd; 17 | @property (unsafe_unretained, nonatomic) IBOutlet UILabel *lb2; 18 | 19 | @end 20 | 21 | @implementation ViewController 22 | 23 | - (instancetype)initWithCoder:(NSCoder *)coder 24 | { 25 | [self configBeforeUse]; 26 | self = [super initWithCoder:coder]; 27 | return self; 28 | } 29 | 30 | - (void)configBeforeUse { 31 | [SFAtStringCore registerAttributes:@{ 32 | NSForegroundColorAttributeName : UIColor.systemBlueColor, 33 | NSFontAttributeName : [UIFont systemFontOfSize:21 weight:(UIFontWeightMedium)], 34 | NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) 35 | } forLabel:@"A"]; 36 | [SFAtStringCore registerAttributes:@{ 37 | NSForegroundColorAttributeName : UIColor.darkGrayColor, 38 | NSFontAttributeName : [UIFont systemFontOfSize:16 weight:(UIFontWeightRegular)], 39 | } forLabel:@"B"]; 40 | [SFAtStringCore registerAttributes:@{ 41 | NSForegroundColorAttributeName : UIColor.systemRedColor, 42 | NSFontAttributeName : [UIFont systemFontOfSize:21 weight:(UIFontWeightMedium)], 43 | NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle) 44 | } forLabel:@"A1"]; 45 | } 46 | 47 | - (void)viewDidLoad { 48 | [super viewDidLoad]; 49 | NSString *string_sf = @"[A][[!]star]012345[[!]star,0,-5.00,21,21]6789[B]][[][[!]][[[[!]star,0,-5.00,21,21]"; 50 | NSLog(@"%@",string_sf.sf_unformattedString); 51 | [_lb2 setSf_text:string_sf]; 52 | [_lb2 setSf_text:string_sf]; 53 | } 54 | 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /OBJC/SFAttributedStringDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // SFAttributedStringDemo 4 | // 5 | // Created by MeterWhite on 2020/7/17. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | NSString * appDelegateClassName; 14 | @autoreleasepool { 15 | // Setup code that might create autoreleased objects goes here. 16 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 17 | } 18 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Logo](https://raw.githubusercontent.com/Meterwhite/SFAttributedString/master/Label.png) 3 | --- 4 | ## First 5 | * This is by far the most leveraged way to output `NSAttributedString`, and it is also the simplest way. The learning cost is 0 and the amount of code is 0. 6 | * like like like, luck luck luck 7 | * 高效率输出`NSAttributedString`,学习成本为0,代码量为0. 8 | * 随手赞一赞,好运上百万 9 | 10 | ## CocoaPods 11 | > objc 12 | ``` 13 | pod 'SFAttributedString' 14 | ``` 15 | > swift 16 | ``` 17 | pod 'SFAttributedStringSwift' 18 | ``` 19 | 20 | ## Simple format attributed string 21 | - The picture below is the output of this very intuitive string 22 | >> 下方图片是这段非常直观的字符串的输出 23 | ```objc 24 | label.attributedText = @"[A]Privacy Policy[B] and [A]Terms of Use".sf_evalString;⤵️ 25 | ``` 26 | ![SFAttributedString icon](https://raw.githubusercontent.com/Meterwhite/SFAttributedString/master/EXP.png) 27 | --- 28 | ```objc 29 | label.attributedText = @"[B]Give [[!]star] to [A]SFAttributedString".sf_evalString;⤵️ 30 | ``` 31 | ![SFAttributedString icon](https://raw.githubusercontent.com/Meterwhite/SFAttributedString/master/TEST_IMG.png) 32 | 33 | ### Format in string(字符串中的格式) 34 | #### Text label 35 | - [ `LABEL` ] 36 | - >Letters, numbers, underscores are allowed 37 | >> 可以使用字母,数字,下划线 38 | ```swift 39 | 40 | "[Normal16]This is[Normal14], SFAttributedString..." 41 | 42 | "[_]This is[16], SFAttributedString..." 43 | 44 | "[N_0x999999_21]This is[M_0x999999_16], SFAttributedString..." 45 | 46 | ``` 47 | #### Image label 48 | - [[!]`IMAGE NAME`] OR [[!]`IMAGE NAME` `, x ,y ,w ,h` ] 49 | ```swift 50 | 51 | "...[[!]hold_person]..." 52 | 53 | "...[[!]hold_person,0,0,15,15]..." 54 | 55 | "...[[!]hold_person,0,0,15.00,15.00]..." 56 | ``` 57 | 58 | ### Registered attributed string label(注册标签) 59 | - All attribute labels need to be registered before use 60 | > objc 61 | ```objc 62 | [SFAtStringCore registerAttributes: forLabel:@"LABEL"]; 63 | ``` 64 | > swift 65 | ```swift 66 | SFAtStringCore.registerAttributes(,forLabel:"LABEL") 67 | ``` 68 | 69 | ### Unformatted string(还原格式) 70 | > objc 71 | ```objc 72 | NSString *unformattedString = .sf_unformattedString; 73 | ``` 74 | > swift 75 | ```swift 76 | let unformattedString = .sf_unformattedString 77 | ``` 78 | 79 | ### XIB supported(支持可视化编辑) 80 | ![IBInspectable icon](https://raw.githubusercontent.com/Meterwhite/SFAttributedString/master/IBEditor.png) 81 | 82 | --- 83 | 84 | ## ? 85 | > I am a developer from China and want to develop outsourcing projects outside of China. 86 | > 成都长期求职 meterwhite@outlook.com 87 | 88 | -------------------------------------------------------------------------------- /SFAttributedString.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SFAttributedString" 3 | s.version = "1.0.1" 4 | s.summary = "This is by far the most leveraged way to output `NSAttributedString`." 5 | s.homepage = "https://github.com/Meterwhite/SFAttributedString" 6 | s.license = "MIT" 7 | s.author = { "Meterwhite" => "meterwhite@outlook.com" } 8 | s.source = { :git => "https://github.com/Meterwhite/SFAttributedString.git", :tag => s.version.to_s } 9 | s.source_files = "OBJC/SFAttributedString/*.{h,m}" 10 | s.requires_arc = true 11 | s.framework = "UIKit" 12 | 13 | s.ios.deployment_target = "7.0" 14 | s.tvos.deployment_target = "9.0" 15 | end 16 | -------------------------------------------------------------------------------- /SFAttributedStringSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SFAttributedStringSwift" 3 | s.version = "1.0.1" 4 | s.summary = "This is by far the most leveraged way to output `NSAttributedString`." 5 | s.homepage = "https://github.com/Meterwhite/SFAttributedString" 6 | s.license = "MIT" 7 | s.author = { "Meterwhite" => "meterwhite@outlook.com" } 8 | s.source = { :git => "https://github.com/Meterwhite/SFAttributedString.git", :tag => s.version.to_s } 9 | s.source_files = "SWIFT/SFAttributedString/*.swift" 10 | s.requires_arc = true 11 | s.swift_version = '5.0' 12 | s.module_name = 'SFAttributedString' 13 | s.framework = "UIKit" 14 | 15 | s.ios.deployment_target = "8.0" 16 | s.tvos.deployment_target = "9.0" 17 | end 18 | -------------------------------------------------------------------------------- /SWIFT/Nothing/SwiftyJSON.swift: -------------------------------------------------------------------------------- 1 | // SwiftyJSON.swift 2 | // 3 | // Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import Foundation 24 | 25 | // MARK: - Error 26 | // swiftlint:disable line_length 27 | public enum SwiftyJSONError: Int, Swift.Error { 28 | case unsupportedType = 999 29 | case indexOutOfBounds = 900 30 | case elementTooDeep = 902 31 | case wrongType = 901 32 | case notExist = 500 33 | case invalidJSON = 490 34 | } 35 | 36 | extension SwiftyJSONError: CustomNSError { 37 | 38 | /// return the error domain of SwiftyJSONError 39 | public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" } 40 | 41 | /// return the error code of SwiftyJSONError 42 | public var errorCode: Int { return self.rawValue } 43 | 44 | /// return the userInfo of SwiftyJSONError 45 | public var errorUserInfo: [String: Any] { 46 | switch self { 47 | case .unsupportedType: 48 | return [NSLocalizedDescriptionKey: "It is an unsupported type."] 49 | case .indexOutOfBounds: 50 | return [NSLocalizedDescriptionKey: "Array Index is out of bounds."] 51 | case .wrongType: 52 | return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."] 53 | case .notExist: 54 | return [NSLocalizedDescriptionKey: "Dictionary key does not exist."] 55 | case .invalidJSON: 56 | return [NSLocalizedDescriptionKey: "JSON is invalid."] 57 | case .elementTooDeep: 58 | return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."] 59 | } 60 | } 61 | } 62 | 63 | // MARK: - JSON Type 64 | 65 | /** 66 | JSON's type definitions. 67 | 68 | See http://www.json.org 69 | */ 70 | public enum Type: Int { 71 | case number 72 | case string 73 | case bool 74 | case array 75 | case dictionary 76 | case null 77 | case unknown 78 | } 79 | 80 | // MARK: - JSON Base 81 | 82 | public struct JSON { 83 | 84 | /** 85 | Creates a JSON using the data. 86 | 87 | - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary 88 | - parameter opt: The JSON serialization reading options. `[]` by default. 89 | 90 | - returns: The created JSON 91 | */ 92 | public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws { 93 | let object: Any = try JSONSerialization.jsonObject(with: data, options: opt) 94 | self.init(jsonObject: object) 95 | } 96 | 97 | /** 98 | Creates a JSON object 99 | - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)` 100 | 101 | - parameter object: the object 102 | 103 | - returns: the created JSON object 104 | */ 105 | public init(_ object: Any) { 106 | switch object { 107 | case let object as Data: 108 | do { 109 | try self.init(data: object) 110 | } catch { 111 | self.init(jsonObject: NSNull()) 112 | } 113 | default: 114 | self.init(jsonObject: object) 115 | } 116 | } 117 | 118 | /** 119 | Parses the JSON string into a JSON object 120 | 121 | - parameter json: the JSON string 122 | 123 | - returns: the created JSON object 124 | */ 125 | public init(parseJSON jsonString: String) { 126 | if let data = jsonString.data(using: .utf8) { 127 | self.init(data) 128 | } else { 129 | self.init(NSNull()) 130 | } 131 | } 132 | 133 | /** 134 | Creates a JSON using the object. 135 | 136 | - parameter jsonObject: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity. 137 | 138 | - returns: The created JSON 139 | */ 140 | fileprivate init(jsonObject: Any) { 141 | object = jsonObject 142 | } 143 | 144 | /** 145 | Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added, 146 | present values getting overwritten, array values getting appended and nested JSONs getting merged the same way. 147 | 148 | - parameter other: The JSON which gets merged into this JSON 149 | 150 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 151 | */ 152 | public mutating func merge(with other: JSON) throws { 153 | try self.merge(with: other, typecheck: true) 154 | } 155 | 156 | /** 157 | Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added, 158 | present values getting overwritten, array values getting appended and nested JSONS getting merged the same way. 159 | 160 | - parameter other: The JSON which gets merged into this JSON 161 | 162 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level. 163 | 164 | - returns: New merged JSON 165 | */ 166 | public func merged(with other: JSON) throws -> JSON { 167 | var merged = self 168 | try merged.merge(with: other, typecheck: true) 169 | return merged 170 | } 171 | 172 | /** 173 | Private woker function which does the actual merging 174 | Typecheck is set to true for the first recursion level to prevent total override of the source JSON 175 | */ 176 | fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws { 177 | if type == other.type { 178 | switch type { 179 | case .dictionary: 180 | for (key, _) in other { 181 | try self[key].merge(with: other[key], typecheck: false) 182 | } 183 | case .array: 184 | self = JSON(arrayValue + other.arrayValue) 185 | default: 186 | self = other 187 | } 188 | } else { 189 | if typecheck { 190 | throw SwiftyJSONError.wrongType 191 | } else { 192 | self = other 193 | } 194 | } 195 | } 196 | 197 | /// Private object 198 | fileprivate var rawArray: [Any] = [] 199 | fileprivate var rawDictionary: [String: Any] = [:] 200 | fileprivate var rawString: String = "" 201 | fileprivate var rawNumber: NSNumber = 0 202 | fileprivate var rawNull: NSNull = NSNull() 203 | fileprivate var rawBool: Bool = false 204 | 205 | /// JSON type, fileprivate setter 206 | public fileprivate(set) var type: Type = .null 207 | 208 | /// Error in JSON, fileprivate setter 209 | public fileprivate(set) var error: SwiftyJSONError? 210 | 211 | /// Object in JSON 212 | public var object: Any { 213 | get { 214 | switch type { 215 | case .array: return rawArray 216 | case .dictionary: return rawDictionary 217 | case .string: return rawString 218 | case .number: return rawNumber 219 | case .bool: return rawBool 220 | default: return rawNull 221 | } 222 | } 223 | set { 224 | error = nil 225 | switch unwrap(newValue) { 226 | case let number as NSNumber: 227 | if number.isBool { 228 | type = .bool 229 | rawBool = number.boolValue 230 | } else { 231 | type = .number 232 | rawNumber = number 233 | } 234 | case let string as String: 235 | type = .string 236 | rawString = string 237 | case _ as NSNull: 238 | type = .null 239 | case nil: 240 | type = .null 241 | case let array as [Any]: 242 | type = .array 243 | rawArray = array 244 | case let dictionary as [String: Any]: 245 | type = .dictionary 246 | rawDictionary = dictionary 247 | default: 248 | type = .unknown 249 | error = SwiftyJSONError.unsupportedType 250 | } 251 | } 252 | } 253 | 254 | /// The static null JSON 255 | @available(*, unavailable, renamed:"null") 256 | public static var nullJSON: JSON { return null } 257 | public static var null: JSON { return JSON(NSNull()) } 258 | } 259 | 260 | /// Private method to unwarp an object recursively 261 | private func unwrap(_ object: Any) -> Any { 262 | switch object { 263 | case let json as JSON: 264 | return unwrap(json.object) 265 | case let array as [Any]: 266 | return array.map(unwrap) 267 | case let dictionary as [String: Any]: 268 | var d = dictionary 269 | dictionary.forEach { pair in 270 | d[pair.key] = unwrap(pair.value) 271 | } 272 | return d 273 | default: 274 | return object 275 | } 276 | } 277 | 278 | public enum Index: Comparable { 279 | case array(Int) 280 | case dictionary(DictionaryIndex) 281 | case null 282 | 283 | static public func == (lhs: Index, rhs: Index) -> Bool { 284 | switch (lhs, rhs) { 285 | case (.array(let left), .array(let right)): return left == right 286 | case (.dictionary(let left), .dictionary(let right)): return left == right 287 | case (.null, .null): return true 288 | default: return false 289 | } 290 | } 291 | 292 | static public func < (lhs: Index, rhs: Index) -> Bool { 293 | switch (lhs, rhs) { 294 | case (.array(let left), .array(let right)): return left < right 295 | case (.dictionary(let left), .dictionary(let right)): return left < right 296 | default: return false 297 | } 298 | } 299 | } 300 | 301 | public typealias JSONIndex = Index 302 | public typealias JSONRawIndex = Index 303 | 304 | extension JSON: Swift.Collection { 305 | 306 | public typealias Index = JSONRawIndex 307 | 308 | public var startIndex: Index { 309 | switch type { 310 | case .array: return .array(rawArray.startIndex) 311 | case .dictionary: return .dictionary(rawDictionary.startIndex) 312 | default: return .null 313 | } 314 | } 315 | 316 | public var endIndex: Index { 317 | switch type { 318 | case .array: return .array(rawArray.endIndex) 319 | case .dictionary: return .dictionary(rawDictionary.endIndex) 320 | default: return .null 321 | } 322 | } 323 | 324 | public func index(after i: Index) -> Index { 325 | switch i { 326 | case .array(let idx): return .array(rawArray.index(after: idx)) 327 | case .dictionary(let idx): return .dictionary(rawDictionary.index(after: idx)) 328 | default: return .null 329 | } 330 | } 331 | 332 | public subscript (position: Index) -> (String, JSON) { 333 | switch position { 334 | case .array(let idx): return (String(idx), JSON(rawArray[idx])) 335 | case .dictionary(let idx): return (rawDictionary[idx].key, JSON(rawDictionary[idx].value)) 336 | default: return ("", JSON.null) 337 | } 338 | } 339 | } 340 | 341 | // MARK: - Subscript 342 | 343 | /** 344 | * To mark both String and Int can be used in subscript. 345 | */ 346 | public enum JSONKey { 347 | case index(Int) 348 | case key(String) 349 | } 350 | 351 | public protocol JSONSubscriptType { 352 | var jsonKey: JSONKey { get } 353 | } 354 | 355 | extension Int: JSONSubscriptType { 356 | public var jsonKey: JSONKey { 357 | return JSONKey.index(self) 358 | } 359 | } 360 | 361 | extension String: JSONSubscriptType { 362 | public var jsonKey: JSONKey { 363 | return JSONKey.key(self) 364 | } 365 | } 366 | 367 | extension JSON { 368 | 369 | /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error. 370 | fileprivate subscript(index index: Int) -> JSON { 371 | get { 372 | if type != .array { 373 | var r = JSON.null 374 | r.error = self.error ?? SwiftyJSONError.wrongType 375 | return r 376 | } else if rawArray.indices.contains(index) { 377 | return JSON(rawArray[index]) 378 | } else { 379 | var r = JSON.null 380 | r.error = SwiftyJSONError.indexOutOfBounds 381 | return r 382 | } 383 | } 384 | set { 385 | if type == .array && 386 | rawArray.indices.contains(index) && 387 | newValue.error == nil { 388 | rawArray[index] = newValue.object 389 | } 390 | } 391 | } 392 | 393 | /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error. 394 | fileprivate subscript(key key: String) -> JSON { 395 | get { 396 | var r = JSON.null 397 | if type == .dictionary { 398 | if let o = rawDictionary[key] { 399 | r = JSON(o) 400 | } else { 401 | r.error = SwiftyJSONError.notExist 402 | } 403 | } else { 404 | r.error = self.error ?? SwiftyJSONError.wrongType 405 | } 406 | return r 407 | } 408 | set { 409 | if type == .dictionary && newValue.error == nil { 410 | rawDictionary[key] = newValue.object 411 | } 412 | } 413 | } 414 | 415 | /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`. 416 | fileprivate subscript(sub sub: JSONSubscriptType) -> JSON { 417 | get { 418 | switch sub.jsonKey { 419 | case .index(let index): return self[index: index] 420 | case .key(let key): return self[key: key] 421 | } 422 | } 423 | set { 424 | switch sub.jsonKey { 425 | case .index(let index): self[index: index] = newValue 426 | case .key(let key): self[key: key] = newValue 427 | } 428 | } 429 | } 430 | 431 | /** 432 | Find a json in the complex data structures by using array of Int and/or String as path. 433 | 434 | Example: 435 | 436 | ``` 437 | let json = JSON[data] 438 | let path = [9,"list","person","name"] 439 | let name = json[path] 440 | ``` 441 | 442 | The same as: let name = json[9]["list"]["person"]["name"] 443 | 444 | - parameter path: The target json's path. 445 | 446 | - returns: Return a json found by the path or a null json with error 447 | */ 448 | public subscript(path: [JSONSubscriptType]) -> JSON { 449 | get { 450 | return path.reduce(self) { $0[sub: $1] } 451 | } 452 | set { 453 | switch path.count { 454 | case 0: return 455 | case 1: self[sub:path[0]].object = newValue.object 456 | default: 457 | var aPath = path 458 | aPath.remove(at: 0) 459 | var nextJSON = self[sub: path[0]] 460 | nextJSON[aPath] = newValue 461 | self[sub: path[0]] = nextJSON 462 | } 463 | } 464 | } 465 | 466 | /** 467 | Find a json in the complex data structures by using array of Int and/or String as path. 468 | 469 | - parameter path: The target json's path. Example: 470 | 471 | let name = json[9,"list","person","name"] 472 | 473 | The same as: let name = json[9]["list"]["person"]["name"] 474 | 475 | - returns: Return a json found by the path or a null json with error 476 | */ 477 | public subscript(path: JSONSubscriptType...) -> JSON { 478 | get { 479 | return self[path] 480 | } 481 | set { 482 | self[path] = newValue 483 | } 484 | } 485 | } 486 | 487 | // MARK: - LiteralConvertible 488 | 489 | extension JSON: Swift.ExpressibleByStringLiteral { 490 | 491 | public init(stringLiteral value: StringLiteralType) { 492 | self.init(value) 493 | } 494 | 495 | public init(extendedGraphemeClusterLiteral value: StringLiteralType) { 496 | self.init(value) 497 | } 498 | 499 | public init(unicodeScalarLiteral value: StringLiteralType) { 500 | self.init(value) 501 | } 502 | } 503 | 504 | extension JSON: Swift.ExpressibleByIntegerLiteral { 505 | 506 | public init(integerLiteral value: IntegerLiteralType) { 507 | self.init(value) 508 | } 509 | } 510 | 511 | extension JSON: Swift.ExpressibleByBooleanLiteral { 512 | 513 | public init(booleanLiteral value: BooleanLiteralType) { 514 | self.init(value) 515 | } 516 | } 517 | 518 | extension JSON: Swift.ExpressibleByFloatLiteral { 519 | 520 | public init(floatLiteral value: FloatLiteralType) { 521 | self.init(value) 522 | } 523 | } 524 | 525 | extension JSON: Swift.ExpressibleByDictionaryLiteral { 526 | public init(dictionaryLiteral elements: (String, Any)...) { 527 | let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1}) 528 | self.init(dictionary) 529 | } 530 | } 531 | 532 | extension JSON: Swift.ExpressibleByArrayLiteral { 533 | 534 | public init(arrayLiteral elements: Any...) { 535 | self.init(elements) 536 | } 537 | } 538 | 539 | // MARK: - Raw 540 | 541 | extension JSON: Swift.RawRepresentable { 542 | 543 | public init?(rawValue: Any) { 544 | if JSON(rawValue).type == .unknown { 545 | return nil 546 | } else { 547 | self.init(rawValue) 548 | } 549 | } 550 | 551 | public var rawValue: Any { 552 | return object 553 | } 554 | 555 | public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data { 556 | guard JSONSerialization.isValidJSONObject(object) else { 557 | throw SwiftyJSONError.invalidJSON 558 | } 559 | 560 | return try JSONSerialization.data(withJSONObject: object, options: opt) 561 | } 562 | 563 | public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? { 564 | do { 565 | return try _rawString(encoding, options: [.jsonSerialization: opt]) 566 | } catch { 567 | print("Could not serialize object to JSON because:", error.localizedDescription) 568 | return nil 569 | } 570 | } 571 | 572 | public func rawString(_ options: [writingOptionsKeys: Any]) -> String? { 573 | let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8 574 | let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10 575 | do { 576 | return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth) 577 | } catch { 578 | print("Could not serialize object to JSON because:", error.localizedDescription) 579 | return nil 580 | } 581 | } 582 | 583 | fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? { 584 | guard maxObjectDepth > 0 else { throw SwiftyJSONError.invalidJSON } 585 | switch type { 586 | case .dictionary: 587 | do { 588 | if !(options[.castNilToNSNull] as? Bool ?? false) { 589 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 590 | let data = try rawData(options: jsonOption) 591 | return String(data: data, encoding: encoding) 592 | } 593 | 594 | guard let dict = object as? [String: Any?] else { 595 | return nil 596 | } 597 | let body = try dict.keys.map { key throws -> String in 598 | guard let value = dict[key] else { 599 | return "\"\(key)\": null" 600 | } 601 | guard let unwrappedValue = value else { 602 | return "\"\(key)\": null" 603 | } 604 | 605 | let nestedValue = JSON(unwrappedValue) 606 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 607 | throw SwiftyJSONError.elementTooDeep 608 | } 609 | if nestedValue.type == .string { 610 | return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 611 | } else { 612 | return "\"\(key)\": \(nestedString)" 613 | } 614 | } 615 | 616 | return "{\(body.joined(separator: ","))}" 617 | } catch _ { 618 | return nil 619 | } 620 | case .array: 621 | do { 622 | if !(options[.castNilToNSNull] as? Bool ?? false) { 623 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted 624 | let data = try rawData(options: jsonOption) 625 | return String(data: data, encoding: encoding) 626 | } 627 | 628 | guard let array = object as? [Any?] else { 629 | return nil 630 | } 631 | let body = try array.map { value throws -> String in 632 | guard let unwrappedValue = value else { 633 | return "null" 634 | } 635 | 636 | let nestedValue = JSON(unwrappedValue) 637 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else { 638 | throw SwiftyJSONError.invalidJSON 639 | } 640 | if nestedValue.type == .string { 641 | return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" 642 | } else { 643 | return nestedString 644 | } 645 | } 646 | 647 | return "[\(body.joined(separator: ","))]" 648 | } catch _ { 649 | return nil 650 | } 651 | case .string: return rawString 652 | case .number: return rawNumber.stringValue 653 | case .bool: return rawBool.description 654 | case .null: return "null" 655 | default: return nil 656 | } 657 | } 658 | } 659 | 660 | // MARK: - Printable, DebugPrintable 661 | 662 | extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible { 663 | 664 | public var description: String { 665 | return rawString(options: .prettyPrinted) ?? "unknown" 666 | } 667 | 668 | public var debugDescription: String { 669 | return description 670 | } 671 | } 672 | 673 | // MARK: - Array 674 | 675 | extension JSON { 676 | 677 | //Optional [JSON] 678 | public var array: [JSON]? { 679 | return type == .array ? rawArray.map { JSON($0) } : nil 680 | } 681 | 682 | //Non-optional [JSON] 683 | public var arrayValue: [JSON] { 684 | return self.array ?? [] 685 | } 686 | 687 | //Optional [Any] 688 | public var arrayObject: [Any]? { 689 | get { 690 | switch type { 691 | case .array: return rawArray 692 | default: return nil 693 | } 694 | } 695 | set { 696 | self.object = newValue ?? NSNull() 697 | } 698 | } 699 | } 700 | 701 | // MARK: - Dictionary 702 | 703 | extension JSON { 704 | 705 | //Optional [String : JSON] 706 | public var dictionary: [String: JSON]? { 707 | if type == .dictionary { 708 | var d = [String: JSON](minimumCapacity: rawDictionary.count) 709 | rawDictionary.forEach { pair in 710 | d[pair.key] = JSON(pair.value) 711 | } 712 | return d 713 | } else { 714 | return nil 715 | } 716 | } 717 | 718 | //Non-optional [String : JSON] 719 | public var dictionaryValue: [String: JSON] { 720 | return dictionary ?? [:] 721 | } 722 | 723 | //Optional [String : Any] 724 | 725 | public var dictionaryObject: [String: Any]? { 726 | get { 727 | switch type { 728 | case .dictionary: return rawDictionary 729 | default: return nil 730 | } 731 | } 732 | set { 733 | object = newValue ?? NSNull() 734 | } 735 | } 736 | } 737 | 738 | // MARK: - Bool 739 | 740 | extension JSON { // : Swift.Bool 741 | 742 | //Optional bool 743 | public var bool: Bool? { 744 | get { 745 | switch type { 746 | case .bool: return rawBool 747 | default: return nil 748 | } 749 | } 750 | set { 751 | object = newValue ?? NSNull() 752 | } 753 | } 754 | 755 | //Non-optional bool 756 | public var boolValue: Bool { 757 | get { 758 | switch type { 759 | case .bool: return rawBool 760 | case .number: return rawNumber.boolValue 761 | case .string: return ["true", "y", "t", "yes", "1"].contains { rawString.caseInsensitiveCompare($0) == .orderedSame } 762 | default: return false 763 | } 764 | } 765 | set { 766 | object = newValue 767 | } 768 | } 769 | } 770 | 771 | // MARK: - String 772 | 773 | extension JSON { 774 | 775 | //Optional string 776 | public var string: String? { 777 | get { 778 | switch type { 779 | case .string: return object as? String 780 | default: return nil 781 | } 782 | } 783 | set { 784 | object = newValue ?? NSNull() 785 | } 786 | } 787 | 788 | //Non-optional string 789 | public var stringValue: String { 790 | get { 791 | switch type { 792 | case .string: return object as? String ?? "" 793 | case .number: return rawNumber.stringValue 794 | case .bool: return (object as? Bool).map { String($0) } ?? "" 795 | default: return "" 796 | } 797 | } 798 | set { 799 | object = newValue 800 | } 801 | } 802 | } 803 | 804 | // MARK: - Number 805 | 806 | extension JSON { 807 | 808 | //Optional number 809 | public var number: NSNumber? { 810 | get { 811 | switch type { 812 | case .number: return rawNumber 813 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 814 | default: return nil 815 | } 816 | } 817 | set { 818 | object = newValue ?? NSNull() 819 | } 820 | } 821 | 822 | //Non-optional number 823 | public var numberValue: NSNumber { 824 | get { 825 | switch type { 826 | case .string: 827 | let decimal = NSDecimalNumber(string: object as? String) 828 | return decimal == .notANumber ? .zero : decimal 829 | case .number: return object as? NSNumber ?? NSNumber(value: 0) 830 | case .bool: return NSNumber(value: rawBool ? 1 : 0) 831 | default: return NSNumber(value: 0.0) 832 | } 833 | } 834 | set { 835 | object = newValue 836 | } 837 | } 838 | } 839 | 840 | // MARK: - Null 841 | 842 | extension JSON { 843 | 844 | public var null: NSNull? { 845 | set { 846 | object = NSNull() 847 | } 848 | get { 849 | switch type { 850 | case .null: return rawNull 851 | default: return nil 852 | } 853 | } 854 | } 855 | public func exists() -> Bool { 856 | if let errorValue = error, (400...1000).contains(errorValue.errorCode) { 857 | return false 858 | } 859 | return true 860 | } 861 | } 862 | 863 | // MARK: - URL 864 | 865 | extension JSON { 866 | 867 | //Optional URL 868 | public var url: URL? { 869 | get { 870 | switch type { 871 | case .string: 872 | // Check for existing percent escapes first to prevent double-escaping of % character 873 | if rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil { 874 | return Foundation.URL(string: rawString) 875 | } else if let encodedString_ = rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) { 876 | // We have to use `Foundation.URL` otherwise it conflicts with the variable name. 877 | return Foundation.URL(string: encodedString_) 878 | } else { 879 | return nil 880 | } 881 | default: 882 | return nil 883 | } 884 | } 885 | set { 886 | object = newValue?.absoluteString ?? NSNull() 887 | } 888 | } 889 | } 890 | 891 | // MARK: - Int, Double, Float, Int8, Int16, Int32, Int64 892 | 893 | extension JSON { 894 | 895 | public var double: Double? { 896 | get { 897 | return number?.doubleValue 898 | } 899 | set { 900 | if let newValue = newValue { 901 | object = NSNumber(value: newValue) 902 | } else { 903 | object = NSNull() 904 | } 905 | } 906 | } 907 | 908 | public var doubleValue: Double { 909 | get { 910 | return numberValue.doubleValue 911 | } 912 | set { 913 | object = NSNumber(value: newValue) 914 | } 915 | } 916 | 917 | public var float: Float? { 918 | get { 919 | return number?.floatValue 920 | } 921 | set { 922 | if let newValue = newValue { 923 | object = NSNumber(value: newValue) 924 | } else { 925 | object = NSNull() 926 | } 927 | } 928 | } 929 | 930 | public var floatValue: Float { 931 | get { 932 | return numberValue.floatValue 933 | } 934 | set { 935 | object = NSNumber(value: newValue) 936 | } 937 | } 938 | 939 | public var int: Int? { 940 | get { 941 | return number?.intValue 942 | } 943 | set { 944 | if let newValue = newValue { 945 | object = NSNumber(value: newValue) 946 | } else { 947 | object = NSNull() 948 | } 949 | } 950 | } 951 | 952 | public var intValue: Int { 953 | get { 954 | return numberValue.intValue 955 | } 956 | set { 957 | object = NSNumber(value: newValue) 958 | } 959 | } 960 | 961 | public var uInt: UInt? { 962 | get { 963 | return number?.uintValue 964 | } 965 | set { 966 | if let newValue = newValue { 967 | object = NSNumber(value: newValue) 968 | } else { 969 | object = NSNull() 970 | } 971 | } 972 | } 973 | 974 | public var uIntValue: UInt { 975 | get { 976 | return numberValue.uintValue 977 | } 978 | set { 979 | object = NSNumber(value: newValue) 980 | } 981 | } 982 | 983 | public var int8: Int8? { 984 | get { 985 | return number?.int8Value 986 | } 987 | set { 988 | if let newValue = newValue { 989 | object = NSNumber(value: Int(newValue)) 990 | } else { 991 | object = NSNull() 992 | } 993 | } 994 | } 995 | 996 | public var int8Value: Int8 { 997 | get { 998 | return numberValue.int8Value 999 | } 1000 | set { 1001 | object = NSNumber(value: Int(newValue)) 1002 | } 1003 | } 1004 | 1005 | public var uInt8: UInt8? { 1006 | get { 1007 | return number?.uint8Value 1008 | } 1009 | set { 1010 | if let newValue = newValue { 1011 | object = NSNumber(value: newValue) 1012 | } else { 1013 | object = NSNull() 1014 | } 1015 | } 1016 | } 1017 | 1018 | public var uInt8Value: UInt8 { 1019 | get { 1020 | return numberValue.uint8Value 1021 | } 1022 | set { 1023 | object = NSNumber(value: newValue) 1024 | } 1025 | } 1026 | 1027 | public var int16: Int16? { 1028 | get { 1029 | return number?.int16Value 1030 | } 1031 | set { 1032 | if let newValue = newValue { 1033 | object = NSNumber(value: newValue) 1034 | } else { 1035 | object = NSNull() 1036 | } 1037 | } 1038 | } 1039 | 1040 | public var int16Value: Int16 { 1041 | get { 1042 | return numberValue.int16Value 1043 | } 1044 | set { 1045 | object = NSNumber(value: newValue) 1046 | } 1047 | } 1048 | 1049 | public var uInt16: UInt16? { 1050 | get { 1051 | return number?.uint16Value 1052 | } 1053 | set { 1054 | if let newValue = newValue { 1055 | object = NSNumber(value: newValue) 1056 | } else { 1057 | object = NSNull() 1058 | } 1059 | } 1060 | } 1061 | 1062 | public var uInt16Value: UInt16 { 1063 | get { 1064 | return numberValue.uint16Value 1065 | } 1066 | set { 1067 | object = NSNumber(value: newValue) 1068 | } 1069 | } 1070 | 1071 | public var int32: Int32? { 1072 | get { 1073 | return number?.int32Value 1074 | } 1075 | set { 1076 | if let newValue = newValue { 1077 | object = NSNumber(value: newValue) 1078 | } else { 1079 | object = NSNull() 1080 | } 1081 | } 1082 | } 1083 | 1084 | public var int32Value: Int32 { 1085 | get { 1086 | return numberValue.int32Value 1087 | } 1088 | set { 1089 | object = NSNumber(value: newValue) 1090 | } 1091 | } 1092 | 1093 | public var uInt32: UInt32? { 1094 | get { 1095 | return number?.uint32Value 1096 | } 1097 | set { 1098 | if let newValue = newValue { 1099 | object = NSNumber(value: newValue) 1100 | } else { 1101 | object = NSNull() 1102 | } 1103 | } 1104 | } 1105 | 1106 | public var uInt32Value: UInt32 { 1107 | get { 1108 | return numberValue.uint32Value 1109 | } 1110 | set { 1111 | object = NSNumber(value: newValue) 1112 | } 1113 | } 1114 | 1115 | public var int64: Int64? { 1116 | get { 1117 | return number?.int64Value 1118 | } 1119 | set { 1120 | if let newValue = newValue { 1121 | object = NSNumber(value: newValue) 1122 | } else { 1123 | object = NSNull() 1124 | } 1125 | } 1126 | } 1127 | 1128 | public var int64Value: Int64 { 1129 | get { 1130 | return numberValue.int64Value 1131 | } 1132 | set { 1133 | object = NSNumber(value: newValue) 1134 | } 1135 | } 1136 | 1137 | public var uInt64: UInt64? { 1138 | get { 1139 | return number?.uint64Value 1140 | } 1141 | set { 1142 | if let newValue = newValue { 1143 | object = NSNumber(value: newValue) 1144 | } else { 1145 | object = NSNull() 1146 | } 1147 | } 1148 | } 1149 | 1150 | public var uInt64Value: UInt64 { 1151 | get { 1152 | return numberValue.uint64Value 1153 | } 1154 | set { 1155 | object = NSNumber(value: newValue) 1156 | } 1157 | } 1158 | } 1159 | 1160 | // MARK: - Comparable 1161 | 1162 | extension JSON: Swift.Comparable {} 1163 | 1164 | public func == (lhs: JSON, rhs: JSON) -> Bool { 1165 | 1166 | switch (lhs.type, rhs.type) { 1167 | case (.number, .number): return lhs.rawNumber == rhs.rawNumber 1168 | case (.string, .string): return lhs.rawString == rhs.rawString 1169 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1170 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1171 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1172 | case (.null, .null): return true 1173 | default: return false 1174 | } 1175 | } 1176 | 1177 | public func <= (lhs: JSON, rhs: JSON) -> Bool { 1178 | 1179 | switch (lhs.type, rhs.type) { 1180 | case (.number, .number): return lhs.rawNumber <= rhs.rawNumber 1181 | case (.string, .string): return lhs.rawString <= rhs.rawString 1182 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1183 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1184 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1185 | case (.null, .null): return true 1186 | default: return false 1187 | } 1188 | } 1189 | 1190 | public func >= (lhs: JSON, rhs: JSON) -> Bool { 1191 | 1192 | switch (lhs.type, rhs.type) { 1193 | case (.number, .number): return lhs.rawNumber >= rhs.rawNumber 1194 | case (.string, .string): return lhs.rawString >= rhs.rawString 1195 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool 1196 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray 1197 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary 1198 | case (.null, .null): return true 1199 | default: return false 1200 | } 1201 | } 1202 | 1203 | public func > (lhs: JSON, rhs: JSON) -> Bool { 1204 | 1205 | switch (lhs.type, rhs.type) { 1206 | case (.number, .number): return lhs.rawNumber > rhs.rawNumber 1207 | case (.string, .string): return lhs.rawString > rhs.rawString 1208 | default: return false 1209 | } 1210 | } 1211 | 1212 | public func < (lhs: JSON, rhs: JSON) -> Bool { 1213 | 1214 | switch (lhs.type, rhs.type) { 1215 | case (.number, .number): return lhs.rawNumber < rhs.rawNumber 1216 | case (.string, .string): return lhs.rawString < rhs.rawString 1217 | default: return false 1218 | } 1219 | } 1220 | 1221 | private let trueNumber = NSNumber(value: true) 1222 | private let falseNumber = NSNumber(value: false) 1223 | private let trueObjCType = String(cString: trueNumber.objCType) 1224 | private let falseObjCType = String(cString: falseNumber.objCType) 1225 | 1226 | // MARK: - NSNumber: Comparable 1227 | 1228 | extension NSNumber { 1229 | fileprivate var isBool: Bool { 1230 | let objCType = String(cString: self.objCType) 1231 | if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) { 1232 | return true 1233 | } else { 1234 | return false 1235 | } 1236 | } 1237 | } 1238 | 1239 | func == (lhs: NSNumber, rhs: NSNumber) -> Bool { 1240 | switch (lhs.isBool, rhs.isBool) { 1241 | case (false, true): return false 1242 | case (true, false): return false 1243 | default: return lhs.compare(rhs) == .orderedSame 1244 | } 1245 | } 1246 | 1247 | func != (lhs: NSNumber, rhs: NSNumber) -> Bool { 1248 | return !(lhs == rhs) 1249 | } 1250 | 1251 | func < (lhs: NSNumber, rhs: NSNumber) -> Bool { 1252 | 1253 | switch (lhs.isBool, rhs.isBool) { 1254 | case (false, true): return false 1255 | case (true, false): return false 1256 | default: return lhs.compare(rhs) == .orderedAscending 1257 | } 1258 | } 1259 | 1260 | func > (lhs: NSNumber, rhs: NSNumber) -> Bool { 1261 | 1262 | switch (lhs.isBool, rhs.isBool) { 1263 | case (false, true): return false 1264 | case (true, false): return false 1265 | default: return lhs.compare(rhs) == ComparisonResult.orderedDescending 1266 | } 1267 | } 1268 | 1269 | func <= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1270 | 1271 | switch (lhs.isBool, rhs.isBool) { 1272 | case (false, true): return false 1273 | case (true, false): return false 1274 | default: return lhs.compare(rhs) != .orderedDescending 1275 | } 1276 | } 1277 | 1278 | func >= (lhs: NSNumber, rhs: NSNumber) -> Bool { 1279 | 1280 | switch (lhs.isBool, rhs.isBool) { 1281 | case (false, true): return false 1282 | case (true, false): return false 1283 | default: return lhs.compare(rhs) != .orderedAscending 1284 | } 1285 | } 1286 | 1287 | public enum writingOptionsKeys { 1288 | case jsonSerialization 1289 | case castNilToNSNull 1290 | case maxObjextDepth 1291 | case encoding 1292 | } 1293 | 1294 | // MARK: - JSON: Codable 1295 | extension JSON: Codable { 1296 | private static var codableTypes: [Codable.Type] { 1297 | return [ 1298 | Bool.self, 1299 | Int.self, 1300 | Int8.self, 1301 | Int16.self, 1302 | Int32.self, 1303 | Int64.self, 1304 | UInt.self, 1305 | UInt8.self, 1306 | UInt16.self, 1307 | UInt32.self, 1308 | UInt64.self, 1309 | Double.self, 1310 | String.self, 1311 | [JSON].self, 1312 | [String: JSON].self 1313 | ] 1314 | } 1315 | public init(from decoder: Decoder) throws { 1316 | var object: Any? 1317 | 1318 | if let container = try? decoder.singleValueContainer(), !container.decodeNil() { 1319 | for type in JSON.codableTypes { 1320 | if object != nil { 1321 | break 1322 | } 1323 | // try to decode value 1324 | switch type { 1325 | case let boolType as Bool.Type: 1326 | object = try? container.decode(boolType) 1327 | case let intType as Int.Type: 1328 | object = try? container.decode(intType) 1329 | case let int8Type as Int8.Type: 1330 | object = try? container.decode(int8Type) 1331 | case let int32Type as Int32.Type: 1332 | object = try? container.decode(int32Type) 1333 | case let int64Type as Int64.Type: 1334 | object = try? container.decode(int64Type) 1335 | case let uintType as UInt.Type: 1336 | object = try? container.decode(uintType) 1337 | case let uint8Type as UInt8.Type: 1338 | object = try? container.decode(uint8Type) 1339 | case let uint16Type as UInt16.Type: 1340 | object = try? container.decode(uint16Type) 1341 | case let uint32Type as UInt32.Type: 1342 | object = try? container.decode(uint32Type) 1343 | case let uint64Type as UInt64.Type: 1344 | object = try? container.decode(uint64Type) 1345 | case let doubleType as Double.Type: 1346 | object = try? container.decode(doubleType) 1347 | case let stringType as String.Type: 1348 | object = try? container.decode(stringType) 1349 | case let jsonValueArrayType as [JSON].Type: 1350 | object = try? container.decode(jsonValueArrayType) 1351 | case let jsonValueDictType as [String: JSON].Type: 1352 | object = try? container.decode(jsonValueDictType) 1353 | default: 1354 | break 1355 | } 1356 | } 1357 | } 1358 | self.init(object ?? NSNull()) 1359 | } 1360 | public func encode(to encoder: Encoder) throws { 1361 | var container = encoder.singleValueContainer() 1362 | if object is NSNull { 1363 | try container.encodeNil() 1364 | return 1365 | } 1366 | switch object { 1367 | case let intValue as Int: 1368 | try container.encode(intValue) 1369 | case let int8Value as Int8: 1370 | try container.encode(int8Value) 1371 | case let int32Value as Int32: 1372 | try container.encode(int32Value) 1373 | case let int64Value as Int64: 1374 | try container.encode(int64Value) 1375 | case let uintValue as UInt: 1376 | try container.encode(uintValue) 1377 | case let uint8Value as UInt8: 1378 | try container.encode(uint8Value) 1379 | case let uint16Value as UInt16: 1380 | try container.encode(uint16Value) 1381 | case let uint32Value as UInt32: 1382 | try container.encode(uint32Value) 1383 | case let uint64Value as UInt64: 1384 | try container.encode(uint64Value) 1385 | case let doubleValue as Double: 1386 | try container.encode(doubleValue) 1387 | case let boolValue as Bool: 1388 | try container.encode(boolValue) 1389 | case let stringValue as String: 1390 | try container.encode(stringValue) 1391 | case is [Any]: 1392 | let jsonValueArray = array ?? [] 1393 | try container.encode(jsonValueArray) 1394 | case is [String: Any]: 1395 | let jsonValueDictValue = dictionary ?? [:] 1396 | try container.encode(jsonValueDictValue) 1397 | default: 1398 | break 1399 | } 1400 | } 1401 | } 1402 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedString/SFAttributedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SFAttributedString.swift 3 | // SFAttributedString 4 | // https://github.com/Meterwhite/SFAttributedString 5 | // 6 | // Created by MeterWhite on 2020/7/18. 7 | // Copyright © 2020 Meterwhite. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | 12 | // MARK: - private vars 13 | fileprivate var cached_txt_ats = Dictionary() 14 | fileprivate var cached_lb_adic = Dictionary() 15 | fileprivate var cached_v_sftxt = NSMapTable(keyOptions: [.weakMemory, .objectPointerPersonality], valueOptions: [.copyIn, .objectPointerPersonality]) 16 | fileprivate var rgx_txt = try! NSRegularExpression(pattern: "\\[\\w+\\]", options: []) 17 | fileprivate var rgx_img = try! NSRegularExpression(pattern: "\\[\\[!\\]\\w+(,(-|\\d|\\.)+)*\\]", options: []) 18 | 19 | 20 | // MARK: - Extension for String 21 | public extension String { 22 | var sf_evalString : NSAttributedString? { 23 | return SFAtStringCore.eval(script: self) 24 | } 25 | var sf_unformattedString : String? { 26 | return SFAtStringCore.unformatted(string: self) 27 | } 28 | } 29 | 30 | // MARK: - SFAtStringCore 31 | public struct SFAtStringCore { 32 | public static func registerAttributes(_ adic: [NSAttributedString.Key : Any], forLabel lb:String) { 33 | cached_lb_adic["[\(lb)]"] = adic 34 | } 35 | 36 | public static func eval(script string:String) -> NSAttributedString? { 37 | /// Cached first 38 | let k = string 39 | if let cached = cached_txt_ats[k] { 40 | return cached 41 | } 42 | /// Split image labels, suspend them.(剥离图片标签,挂起图片标签相关信息) 43 | var string = string 44 | var cks_txt = rgx_txt.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) 45 | let cks_img = rgx_img.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) 46 | var img_atms = [SFImageLbAttachment]() 47 | if cks_img.count > 0 { 48 | img_atms += atmsForImgLb(string, txtCks: cks_txt) 49 | var tstring = string 50 | for item in cks_img.reversed() { 51 | if let ran = Range.init(item.range, in: tstring) { 52 | tstring.removeSubrange(ran) 53 | } 54 | } 55 | string = tstring 56 | } 57 | /// Here begins to parse the plain text(这里开始解析纯文本) 58 | cks_txt = rgx_txt.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) 59 | cks_txt = cks_txt.filter { (rt) -> Bool in 60 | return (cached_lb_adic[String(string[Range.init(rt.range, in: string)!])] != nil) 61 | } 62 | let ret = NSMutableAttributedString() 63 | var txtRange = NSRange() 64 | guard cks_txt.count > 0 else { 65 | assert(false, "Missing attribute label!") 66 | return nil 67 | } 68 | guard cks_txt.first!.range.location == 0 || 69 | nil != cached_lb_adic[String(string[Range.init(cks_txt.first!.range, in: string)!])] else { 70 | assert(false, "The first attribute label is missing!") 71 | return nil 72 | } 73 | for i in 0.. 0 { 93 | for atm in img_atms.reversed() { 94 | ret.insert(NSAttributedString(attachment: atm), at: atm.inserdex) 95 | } 96 | } 97 | cached_txt_ats[k] = ret as NSAttributedString 98 | return ret as NSAttributedString 99 | } 100 | 101 | public static func unformatted(string txt: String) -> String? { 102 | var ret = txt 103 | var cks_txt = rgx_txt.matches(in: txt, options: [], range: NSRange(location: 0, length: txt.count)) 104 | cks_txt = cks_txt.filter { (rt) -> Bool in 105 | return (cached_lb_adic[String(txt[Range.init(rt.range, in: txt)!])] != nil) 106 | } 107 | let cks_img = rgx_img.matches(in: txt, options: [], range: NSRange(location: 0, length: txt.count)) 108 | let cks = combineCks(txt: cks_txt, img: cks_img) 109 | guard cks.count > 0 else { 110 | assert(false, "Missing attribute label!") 111 | return nil 112 | } 113 | guard cks.first?.range.location ?? 0 == 0 || 114 | nil != cached_lb_adic[String(txt[Range.init(cks.first!.range, in: txt)!])] else { 115 | assert(false, "The first attribute label is missing!") 116 | return nil 117 | } 118 | for item in cks.reversed() { 119 | ret.removeSubrange(Range.init(item.range, in: txt)!) 120 | } 121 | return ret 122 | } 123 | } 124 | 125 | // MARK: - IB Inspectable supported 126 | public extension UITextField { 127 | @IBInspectable override var sf_text : String? { 128 | get { 129 | super.sf_text 130 | } 131 | set { 132 | super.sf_text = newValue 133 | self.attributedText = newValue?.sf_evalString 134 | } 135 | } 136 | } 137 | 138 | public extension UITextView { 139 | @IBInspectable override var sf_text : String? { 140 | get { 141 | super.sf_text 142 | } 143 | set { 144 | super.sf_text = newValue 145 | self.attributedText = newValue?.sf_evalString 146 | } 147 | } 148 | } 149 | 150 | public extension UIButton { 151 | @IBInspectable var sf_title_default : String? { 152 | get { 153 | if let userInfo = cached_v_sftxt.object(forKey: self) as? [UInt : String] { 154 | return userInfo[UIControl.State.normal.rawValue] 155 | } 156 | return nil 157 | } 158 | set { 159 | let userInfo = cached_v_sftxt.object(forKey: self) 160 | var udic : [UInt : String] 161 | if let x = userInfo as? [UInt : String] { 162 | udic = x 163 | } else { 164 | udic = [UInt : String]() 165 | } 166 | udic[UIControl.State.normal.rawValue] = newValue 167 | cached_v_sftxt.setObject(udic as AnyObject?, forKey: self) 168 | self.setAttributedTitle(newValue?.sf_evalString, for: .normal) 169 | } 170 | } 171 | @IBInspectable var sf_title_highlighted : String? { 172 | get { 173 | if let userInfo = cached_v_sftxt.object(forKey: self) as? [UInt : String] { 174 | return userInfo[UIControl.State.highlighted.rawValue] 175 | } 176 | return nil 177 | } 178 | set { 179 | let userInfo = cached_v_sftxt.object(forKey: self) 180 | var udic : [UInt : String] 181 | if let x = userInfo as? [UInt : String] { 182 | udic = x 183 | } else { 184 | udic = [UInt : String]() 185 | } 186 | udic[UIControl.State.highlighted.rawValue] = newValue 187 | cached_v_sftxt.setObject(udic as AnyObject?, forKey: self) 188 | self.setAttributedTitle(newValue?.sf_evalString, for: .highlighted) 189 | } 190 | } 191 | @IBInspectable var sf_title_selected : String? { 192 | get { 193 | if let userInfo = cached_v_sftxt.object(forKey: self) as? [UInt : String] { 194 | return userInfo[UIControl.State.selected.rawValue] 195 | } 196 | return nil 197 | } 198 | set { 199 | let userInfo = cached_v_sftxt.object(forKey: self) 200 | var udic : [UInt : String] 201 | if let x = userInfo as? [UInt : String] { 202 | udic = x 203 | } else { 204 | udic = [UInt : String]() 205 | } 206 | udic[UIControl.State.selected.rawValue] = newValue 207 | cached_v_sftxt.setObject(udic as AnyObject?, forKey: self) 208 | self.setAttributedTitle(newValue?.sf_evalString, for: .selected) 209 | } 210 | } 211 | @IBInspectable var sf_title_disabled : String? { 212 | get { 213 | if let userInfo = cached_v_sftxt.object(forKey: self) as? [UInt : String] { 214 | return userInfo[UIControl.State.disabled.rawValue] 215 | } 216 | return nil 217 | } 218 | set { 219 | let userInfo = cached_v_sftxt.object(forKey: self) 220 | var udic : [UInt : String] 221 | if let x = userInfo as? [UInt : String] { 222 | udic = x 223 | } else { 224 | udic = [UInt : String]() 225 | } 226 | udic[UIControl.State.disabled.rawValue] = newValue 227 | cached_v_sftxt.setObject(udic as AnyObject?, forKey: self) 228 | self.setAttributedTitle(newValue?.sf_evalString, for: .disabled) 229 | } 230 | } 231 | } 232 | 233 | public extension UILabel { 234 | @IBInspectable override var sf_text : String? { 235 | get { 236 | super.sf_text 237 | } 238 | set { 239 | super.sf_text = newValue 240 | self.attributedText = newValue?.sf_evalString 241 | } 242 | } 243 | } 244 | 245 | // MARK: - Subclass 246 | fileprivate class SFImageLbAttachment : NSTextAttachment { 247 | var conRange : Range? 248 | var offset : Int? 249 | var inserdex : Int { 250 | if let xRan = conRange, let xOff = offset { 251 | return xRan.first! - xOff 252 | } 253 | return 0 254 | } 255 | } 256 | 257 | // MARK: - Private extension 258 | fileprivate extension UIView { 259 | @objc var sf_text : String? { 260 | get { 261 | cached_v_sftxt.object(forKey: self) as? String 262 | } 263 | set { 264 | cached_v_sftxt.setObject(newValue as AnyObject, forKey: self) 265 | } 266 | } 267 | } 268 | 269 | // MARK: - Discrete func 270 | 271 | fileprivate func combineCks(txt txtCks: [NSTextCheckingResult], img imgCks:[NSTextCheckingResult]) -> [NSTextCheckingResult] { 272 | if 0 == txtCks.count { 273 | return imgCks 274 | } 275 | if 0 == imgCks.count { 276 | return txtCks 277 | } 278 | var a = [NSTextCheckingResult]() 279 | a += txtCks 280 | a += imgCks 281 | a.sort { (obj1, obj2) -> Bool in 282 | if obj1.range.location < obj2.range.location { 283 | return true 284 | } 285 | return false 286 | } 287 | return a 288 | } 289 | 290 | @inline(__always) fileprivate func setAtm(_ atm: SFImageLbAttachment,from imgLb: String) { 291 | let start = imgLb.index(imgLb.startIndex, offsetBy: 4) 292 | let con = imgLb[start.. [SFImageLbAttachment] { 315 | var ret = [SFImageLbAttachment]() 316 | var offset = 0 317 | var nonTxtLbString = string 318 | for item in cks_txt.reversed() { 319 | if let ran = Range.init(item.range, in: string) { 320 | if nil != cached_lb_adic[String(nonTxtLbString[ran])] { 321 | nonTxtLbString.removeSubrange(ran) 322 | } 323 | } 324 | } 325 | let cks_img = rgx_img.matches(in: nonTxtLbString, options: [], range: NSRange(location: 0, length: nonTxtLbString.count)) 326 | for i in 0.. 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SFAttributedStringSwiftDemo 4 | // 5 | // Created by MeterWhite on 2020/7/18. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Assets.xcassets/star.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "star@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Assets.xcassets/star.imageset/star@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/SWIFT/SFAttributedStringSwiftDemo/Assets.xcassets/star.imageset/star@2x.png -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 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. 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SFAttributedStringSwiftDemo 4 | // 5 | // Created by MeterWhite on 2020/7/18. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /SWIFT/SFAttributedStringSwiftDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SFAttributedStringSwiftDemo 4 | // 5 | // Created by MeterWhite on 2020/7/18. 6 | // Copyright © 2020 Meterwhite. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var lb: UILabel! 14 | 15 | @IBOutlet weak var bt: UIButton! 16 | 17 | @IBOutlet weak var txtv: UITextView! 18 | 19 | @IBOutlet weak var txtfd: UITextField! 20 | 21 | @IBOutlet weak var lb2: UILabel! 22 | 23 | required init?(coder: NSCoder) { 24 | ViewController.configBeforeUse() 25 | super.init(coder: coder) 26 | } 27 | 28 | static func configBeforeUse() { 29 | SFAtStringCore.registerAttributes([ 30 | NSAttributedString.Key.foregroundColor : UIColor.systemBlue, 31 | NSAttributedString.Key.font : UIFont.systemFont(ofSize: 21, weight: .medium), 32 | NSAttributedString.Key.underlineStyle:NSUnderlineStyle.single.rawValue 33 | ], forLabel: "A") 34 | SFAtStringCore.registerAttributes([ 35 | NSAttributedString.Key.foregroundColor : UIColor.darkGray, 36 | NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16, weight: .regular), 37 | ], forLabel: "B") 38 | SFAtStringCore.registerAttributes([ 39 | NSAttributedString.Key.foregroundColor : UIColor.systemRed, 40 | NSAttributedString.Key.font : UIFont.systemFont(ofSize: 21, weight: .medium), 41 | NSAttributedString.Key.underlineStyle:NSUnderlineStyle.single.rawValue 42 | ], forLabel: "A1") 43 | } 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | let sf = "[A][[!]star]012345[[!]star,0,-5.00,21,21]6789[B]][[][[!]][[[[!]star,0,-5.00,21,21]" 48 | print(sf.sf_unformattedString as Any) 49 | lb2.sf_text = sf 50 | lb2.sf_text = sf 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /TEST_IMG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meterwhite/SFAttributedString/c7b320895d81ea5e421dd3322dfaebe46f6e8846/TEST_IMG.png --------------------------------------------------------------------------------