I want to replace a substring (e.g. #"replace") of an NSAttributedString with another NSAttributedString.
I am looking for an equivalent method to NSString's stringByReplacingOccurrencesOfString:withString: for NSAttributedString.
Convert your attributed string into an instance of NSMutableAttributedString.
The mutable attributed string has a mutableString property. According to the documentation:
"The receiver tracks changes to this string and keeps its attribute mappings up to date."
So you can use the resulting mutable string to execute the replacement with replaceOccurrencesOfString:withString:options:range:.
Here is how you can change the string of NSMutableAttributedString, while preserving its attributes:
Swift:
// first we create a mutable copy of attributed text
let originalAttributedText = nameLabel.attributedText?.mutableCopy() as! NSMutableAttributedString
// then we replace text so easily
let newAttributedText = originalAttributedText.mutableString.setString("new text to replace")
Objective-C:
NSMutableAttributedString *newAttrStr = [attribtedTxt.mutableString setString:#"new string"];
In my case, the following way was the only (tested on iOS9):
NSAttributedString *attributedString = ...;
NSAttributedString *anotherAttributedString = ...; //the string which will replace
while ([attributedString.mutableString containsString:#"replace"]) {
NSRange range = [attributedString.mutableString rangeOfString:#"replace"];
[attributedString replaceCharactersInRange:range withAttributedString:anotherAttributedString];
}
Of course it will be nice to find another better way.
Swift 4:
Updated sunkas excellent solution to Swift 4 and wrapped in "extension". Just clip this into your ViewController (outside the class) and use it.
extension NSAttributedString {
func stringWithString(stringToReplace: String, replacedWithString newStringPart: String) -> NSMutableAttributedString
{
let mutableAttributedString = mutableCopy() as! NSMutableAttributedString
let mutableString = mutableAttributedString.mutableString
while mutableString.contains(stringToReplace) {
let rangeOfStringToBeReplaced = mutableString.range(of: stringToReplace)
mutableAttributedString.replaceCharacters(in: rangeOfStringToBeReplaced, with: newStringPart)
}
return mutableAttributedString
}
}
With Swift 4 and iOS 11, you can use one of the 2 following ways in order to solve your problem.
#1. Using NSMutableAttributedString replaceCharacters(in:with:) method
NSMutableAttributedString has a method called replaceCharacters(in:with:). replaceCharacters(in:with:) has the following declaration:
Replaces the characters and attributes in a given range with the characters and attributes of the given attributed string.
func replaceCharacters(in range: NSRange, with attrString: NSAttributedString)
The Playground code below shows how to use replaceCharacters(in:with:) in order to replace a substring of an NSMutableAttributedString instance with a new NSMutableAttributedString instance:
import UIKit
// Set initial attributed string
let initialString = "This is the initial string"
let attributes = [NSAttributedStringKey.foregroundColor : UIColor.red]
let mutableAttributedString = NSMutableAttributedString(string: initialString, attributes: attributes)
// Set new attributed string
let newString = "new"
let newAttributes = [NSAttributedStringKey.underlineStyle : NSUnderlineStyle.styleSingle.rawValue]
let newAttributedString = NSMutableAttributedString(string: newString, attributes: newAttributes)
// Get range of text to replace
guard let range = mutableAttributedString.string.range(of: "initial") else { exit(0) }
let nsRange = NSRange(range, in: mutableAttributedString.string)
// Replace content in range with the new content
mutableAttributedString.replaceCharacters(in: nsRange, with: newAttributedString)
#2. Using NSMutableString replaceOccurrences(of:with:options:range:) method
NSMutableString has a method called replaceOccurrences(of:with:options:range:). replaceOccurrences(of:with:options:range:) has the following declaration:
Replaces all occurrences of a given string in a given range with another given string, returning the number of replacements.
func replaceOccurrences(of target: String, with replacement: String, options: NSString.CompareOptions = [], range searchRange: NSRange) -> Int
The Playground code below shows how to use replaceOccurrences(of:with:options:range:) in order to replace a substring of an NSMutableAttributedString instance with a new NSMutableAttributedString instance:
import UIKit
// Set initial attributed string
let initialString = "This is the initial string"
let attributes = [NSAttributedStringKey.foregroundColor : UIColor.red]
let mutableAttributedString = NSMutableAttributedString(string: initialString, attributes: attributes)
// Set new string
let newString = "new"
// Replace replaceable content in mutableAttributedString with new content
let totalRange = NSRange(location: 0, length: mutableAttributedString.string.count)
_ = mutableAttributedString.mutableString.replaceOccurrences(of: "initial", with: newString, options: [], range: totalRange)
// Get range of text that requires new attributes
guard let range = mutableAttributedString.string.range(of: newString) else { exit(0) }
let nsRange = NSRange(range, in: mutableAttributedString.string)
// Apply new attributes to the text matching the range
let newAttributes = [NSAttributedStringKey.underlineStyle : NSUnderlineStyle.styleSingle.rawValue]
mutableAttributedString.setAttributes(newAttributes, range: nsRange)
I had to bold text in <b> tags, here what I've done:
- (NSAttributedString *)boldString:(NSString *)string {
UIFont *boldFont = [UIFont boldSystemFontOfSize:14];
NSMutableAttributedString *attributedDescription = [[NSMutableAttributedString alloc] initWithString:string];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#".*?<b>(.*?)<\\/b>.*?" options:NSRegularExpressionCaseInsensitive error:NULL];
NSArray *myArray = [regex matchesInString:string options:0 range:NSMakeRange(0, string.length)] ;
for (NSTextCheckingResult *match in myArray) {
NSRange matchRange = [match rangeAtIndex:1];
[attributedDescription addAttribute:NSFontAttributeName value:boldFont range:matchRange];
}
while ([attributedDescription.string containsString:#"<b>"] || [attributedDescription.string containsString:#"</b>"]) {
NSRange rangeOfTag = [attributedDescription.string rangeOfString:#"<b>"];
[attributedDescription replaceCharactersInRange:rangeOfTag withString:#""];
rangeOfTag = [attributedDescription.string rangeOfString:#"</b>"];
[attributedDescription replaceCharactersInRange:rangeOfTag withString:#""];
}
return attributedDescription;
}
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:#"I am a boy."];
[result addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(0, [result length])];
NSMutableAttributedString *replace = [[NSMutableAttributedString alloc] initWithString:#"a"];
[replace addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, [replace length])];
[result replaceCharactersInRange:NSMakeRange(5, [replace length]) withAttributedString:replace];
I find that all of the other answers does not work. Here is how I replaced content of a NSAttributed string in a category extension:
func stringWithString(stringToReplace:String, replacedWithString newStringPart:String) -> NSMutableAttributedString
{
let mutableAttributedString = mutableCopy() as! NSMutableAttributedString
let mutableString = mutableAttributedString.mutableString
while mutableString.containsString(stringToReplace) {
let rangeOfStringToBeReplaced = mutableString.rangeOfString(stringToReplace)
mutableAttributedString.replaceCharactersInRange(rangeOfStringToBeReplaced, withString: newStringPart)
}
return mutableAttributedString
}
I have a specific requirement and fixed like below. This might help someone.
Requirement: In the storyboard, rich text directly added to UITextView's attribute which contains a word "App Version: 1.0". Now I have to dynamise the version number by reading it from info plist.
Solution:
Deleted version number 1.0 from the storyboard, just kept "App Version:" and added below code.
NSAttributedString *attribute = self.firsttextView.attributedText;
NSMutableAttributedString *mutableAttri = [[NSMutableAttributedString alloc] initWithAttributedString:attribute];
NSString *appVersionText = #"App Version:";
if ([[mutableAttri mutableString] containsString:appVersionText]) {
NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary];
NSString* version = [infoDict objectForKey:#"CFBundleShortVersionString"];
NSString *newappversion = [NSString stringWithFormat:#"%# %#",appVersionText,version] ;
[[mutableAttri mutableString] replaceOccurrencesOfString:appVersionText withString:newappversion options:NSCaseInsensitiveSearch range:NSMakeRange(0, mutableAttri.length)];
self.firsttextView.attributedText = mutableAttri;
}
Done!! Updated/modified attributedText.
i created a Swift 5 extension for that
extension NSMutableAttributedString {
func replace(_ findString: String, with replacement: String, attributes: [NSAttributedString.Key : Any]) {
let ms = mutableString
var range = ms.range(of: findString)
while range.location != NSNotFound {
addAttributes(attributes, range: range)
ms.replaceCharacters(in: range, with: replacement)
range = ms.range(of: findString)
}
}
}
use case
attributedString.replace("%EMAIL%", with: email, attributes: [.font:boldFont])
Related
Hello All & Thanks in Advance!
I am a noob with Core Data and I need to change data in one of my fields which is named: recid.
I have created a index which again is: recid as int 16 in my core data model.
What I am needing to do is fetch the record and changed recid from we will say 5 to 1 how would I go about doing this?
Here is the code I have built so far & I will take care of my loop after I understand how to change the data in the record.
-(void)awakeFromNib
{
NSMenu *theMenu;
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain];
[statusItem setMenu:statusMenu];
[statusItem setImage:[NSImage imageNamed:#"TheJournal_16x16x32"]];
[statusItem setHighlightMode:YES];
theMenu = [[NSMenu alloc] initWithTitle:#""];
[theMenu addItemWithTitle:#"The Journal" action:#selector(showTheWindow:) keyEquivalent:#"W"];
[theMenu addItemWithTitle:#"Quit" action:#selector(terminate:) keyEquivalent:#"Q"];
[statusItem setMenu:theMenu];
[theMenu release];
NSUInteger count;
count = 0;
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
count = [prefs integerForKey:#"recid"];
NSLog(#"counter is >>>>%lu",(unsigned long)count);
[prefs setInteger:count forKey:#"recid"];
count++;
NSUserDefaults *prefs1 = [NSUserDefaults standardUserDefaults];
[prefs1 setInteger:count forKey:#"recid"];
NSLog(#"counter is >>>>%lu",(unsigned long)count);
// How I fetch the record & change the value from 5 to 1?
}
Here's a code sample that may help you. In this example, the YourManagedClass Core Data entity uses a UUID string as a unique record identifier. The extension contains a static function that fetches the unique record, sets the new recid value and then saves the NSManagedObjectContext.
import Foundation
import CoreData
class YourManagedClass: NSManagedObject {
#NSManaged var uuid: String?
#NSManaged var recid: NSNumber?
}
extension YourManagedClass {
static func set(recID: Int16, forObject uuid: String, `in` context: NSManagedObjectContext) {
let fetchRequest = NSFetchRequest<YourManagedClass>(entityName: "YourManagedClass")
fetchRequest.predicate = NSPredicate(format: "uuid = %#", argumentArray: [uuid])
let object: YourManagedClass
do {
let objects = try context.fetch(fetchRequest)
guard let foundObject = objects.first else {
return
}
object = foundObject
} catch {
// Handle Error
return
}
object.recid = NSNumber(value: recID)
do {
try context.save()
} catch {
// Handle Error
}
}
}
You would then call this function with a reference to your NSManagedObjectContext ('context' here):
YourManagedClass.set(recID: 15, forObject: "909455F3-C812-4399-83B4-F96A5C32A71D", in: context)
I have a huge text in String.
For example "... value=word. ...". How can I get the string "word" if I know that before I have "value=" and after "."?
for example:
for str in string {
if str == "value=" {
// then get the strings until .
}
}
Thanks!
You can extend String with a kind of sliceBetween method:
import Foundation
extension String {
func sliceFrom(start: String, to: String) -> String? {
guard let s = rangeOfString(start)?.endIndex else { return nil }
guard let e = rangeOfString(to, range: s..<endIndex)?.startIndex else { return nil }
return self[s..<e]
}
}
And you'd use it like this:
"... value=word. ...".sliceFrom("value=", to: ". ") // "word"
NSRegularExpression should solve your issue.
In order to use it, you will need to understand Regex first. In your case, you can use value=[\\w]+[^.]+ as your regex pattern.
The following code will give you a [String] object contains value=allCharacterBeforeFirstPeriod
let regex = try NSRegularExpression(pattern: "value=[\\w]+[^.]+", options: [])
let nsStr = str as NSString
let array = regex.matchesInString(str, options: [], range: NSMakeRange(0, nsStr.length))
let results = array.map({ nsStr.substringWithRange($0.range) })
And then if you only need the value after value=, you can use another map function to do it:
results.map({ $0.stringByReplacingOccurrencesOfString("value=", withString: "") })
I have tested the code with a 10,000 characters String. It finishes in ~0.3 sec
The most straight forward way to do this would be to use NSRegularExpression. Tutorial
Given an input String like this
let text = "key0=value0&key1=value1&key2=value2"
You can organireduce method
let dict = text.characters.split("&").reduce([String:String]()) { (var result, keyValue) -> [String:String] in
let chunks = keyValue.split("=")
guard let first = chunks.first, last = chunks.last else { return result }
let key = String(first)
let value = String(last)
result[key] = value
return result
}
Now everything is stored inside dict and you can easily access it
dict["key2"] // "value2"
I need to know if a string contains an Int to be sure that a name the user entered is a valid full name,
for that I need to either make the user type only chars, or valid that there are no ints in the string the user entered.
Thanks for all the help.
You can use Foundation methods with Swift strings, and that's what you should do here. NSString has built in methods that use NSCharacterSet to check if certain types of characters are present. This translates nicely to Swift:
var str = "Hello, playground1"
let decimalCharacters = CharacterSet.decimalDigits
let decimalRange = str.rangeOfCharacter(from: decimalCharacters)
if decimalRange != nil {
print("Numbers found")
}
If you're interested in restricting what can be typed, you should implement UITextFieldDelegate and the method textField(_:shouldChangeCharactersIn:replacementString:) to prevent people from typing those characters in the first place.
Simple Swift 4 version using rangeOfCharacter method from String class:
let numbersRange = stringValue.rangeOfCharacter(from: .decimalDigits)
let hasNumbers = (numbersRange != nil)
This method is what i use now for checking if a string contains a number
func doStringContainsNumber( _string : String) -> Bool{
let numberRegEx = ".*[0-9]+.*"
let testCase = NSPredicate(format:"SELF MATCHES %#", numberRegEx)
let containsNumber = testCase.evaluateWithObject(_string)
return containsNumber
}
If your string Contains a number it will return true else false. Hope it helps
//Swift 3.0 to check if String contains numbers (decimal digits):
let someString = "string 1"
let numberCharacters = NSCharacterSet.decimalDigits
if someString.rangeOfCharacter(from: numberCharacters) != nil
{ print("String contains numbers")}
else if someString.rangeOfCharacter(from: numberCharacters) == nil
{ print("String doesn't contains numbers")}
//A function that checks if a string has any numbers
func stringHasNumber(_ string:String) -> Bool {
for character in string{
if character.isNumber{
return true
}
}
return false
}
/// Check stringHasNumber function
stringHasNumber("mhhhldiddld")
stringHasNumber("kjkdjd99900")
if (ContainsNumbers(str).count > 0)
{
// Your string contains at least one number 0-9
}
func ContainsNumbers(s: String) -> [Character]
{
return s.characters.filter { ("0"..."9").contains($0)}
}
Swift 2.3. version working.
extension String
{
func containsNumbers() -> Bool
{
let numberRegEx = ".*[0-9]+.*"
let testCase = NSPredicate(format:"SELF MATCHES %#", numberRegEx)
return testCase.evaluateWithObject(self)
}
}
Usage:
//guard let firstname = textField.text else { return }
let testStr1 = "lalalala"
let testStr2 = "1lalalala"
let testStr3 = "lal2lsd2l"
print("Test 1 = \(testStr1.containsNumbers())\nTest 2 = \(testStr2.containsNumbers())\nTest 3 = \(testStr3.containsNumbers())\n")
You need to trick Swift into using Regex by wrapping up its nsRegularExpression
class Regex {
let internalExpression: NSRegularExpression
let pattern: String
init(_ pattern: String) {
self.pattern = pattern
var error: NSError?
self.internalExpression = NSRegularExpression(pattern: pattern, options: .CaseInsensitive, error: &error)
}
func test(input: String) -> Bool {
let matches = self.internalExpression.matchesInString(input, options: nil, range:NSMakeRange(0, countElements(input)))
return matches.count > 0
}
}
if Regex("\\d/").test("John 2 Smith") {
println("has a number in the name")
}
I got these from http://benscheirman.com/2014/06/regex-in-swift/
let numericCharSet = CharacterSet.init(charactersIn: "1234567890")
let newCharSet = CharacterSet.init(charactersIn: "~`##$%^&*(){}[]<>?")
let sentence = "Tes#ting4 #Charact2er1Seqt"
if sentence.rangeOfCharacter(from: numericCharSet) != nil {
print("Yes It,Have a Numeric")
let removedSpl = sentence.components(separatedBy: newCharSet).joined()
print(sentence.components(separatedBy: newCharSet).joined())
print(removedSpl.components(separatedBy: numericCharSet).joined())
}
else {
print("No")
}
I am trying to fetch some data from Core Data and have run into a slight problem. I can fetch the data with no problem. The moment I try to grab a specific piece of data (i.e. data.fooBar), it throws up an error:
"'AnyObject' does not have a member name 'fooBar'
If I println(data) it will show that fooBar does exist with data stored in it.
I am not really sure why it is doing this. I have tried to search for an answer and tried a bunch of different things but none have seemed to work. Any help would be great. Thanks. :)
var results : Array<AnyObject> = []
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
//get the data for that storedItem
var appDel:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
var context:NSManagedObjectContext = appDel.managedObjectContext!
let req = NSFetchRequest(entityName: "storedItems")
let name:String = results[indexPath.row].name
req.predicate = NSPredicate(format: "name == %#", name)
req.returnsObjectsAsFaults = false
var tapResults = context.executeFetchRequest(req, error: nil)!
for item in tapResults {
println(item) //works, shows all data correctly(including subText)
println(item.name) //works, the only one that does for some reason???
println(item.subText) //Error 'AnyObject' does not have a member name 'subText'
}
Here is the result for: println(item)
println(item) <NSManagedObject: 0x7f04be60> (entity: storedItems; id: 0x7f041de0 <x-coredata://DD4F8E68-2234-46B5-B1D8-AE2F75245C63/storedItems/p1> ; data: {
alarmSound = default;
isDefault = 0;
name = "test";
sliderHours = 0;
sliderMinutes = 0;
sliderSeconds = 0;
subText = "00:00:00";
UPDATE: Based on discussion over vacawama answer (Thank you Aaron). For correct solution please see the answer I accepted.
my itemObj class
#objc(itemObj)
class itemObj: NSManagedObject {
#NSManaged var name:String!
#NSManaged var sliderHours:NSNumber
#NSManaged var sliderMinutes:NSNumber
#NSManaged var sliderSeconds:NSNumber
#NSManaged var subText:String!
#NSManaged var alarmSound:String!
#NSManaged var isDefault:NSNumber
}
my AddItem VC:
var tResults = (context.executeFetchRequest(req, error: nil))
for item in tResults as [itemObj!] {
println(item.name)
println(item.subText)
}
executeFetchRequest returns an optional array of AnyObject. You shouldn't force-unwrap it (this can cause a crash). So optionally unwrap it and do an optional cast (as?) to make sure the type is correct:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context:NSManagedObjectContext = appDel.managedObjectContext!
let req = NSFetchRequest(entityName: "storedItems")
let name:String = results[indexPath.row].name
req.predicate = NSPredicate(format: "name == %#", name)
req.returnsObjectsAsFaults = false
let tapResults = context.executeFetchRequest(req, error: nil)
if let presentResults = tapResults {
if let castedResults = presentResults as? [MyManagedObjectSubclass] {
for item in castedResults {
println(item)
println(item.name)
println(item.subText)
}
}
}
}
I also changed all of your vars to lets since they don't need to be mutable.
Just replace MyManagedObjectSubclass with whatever your NSManagedObject subclass is.
I am trying to get the string for "content" but when I execute the code tha application crash... when I try with self.content.text = "\(theText)" it works but I want to encode to UTF8 because I use cyrilic and I get back kind of these \U0421\U044a\U0434\U044a\U0440\U0436\U0430\U043d\U0438\U0435 \U043d\U0430 \U043f\U0438\U0449\U043e\U0432\U0430 Anyone who can fix the issue?
Here is the code:
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context: NSManagedObjectContext = appDel.managedObjectContext
let theReq = NSFetchRequest(entityName: "Entity1")
theReq.returnsObjectsAsFaults = false
let myPredicate = NSPredicate(format: "objectId == \"\(contentID)\"")
theReq.predicate = myPredicate
let executed:AnyObject = context.executeFetchRequest(theReq, error: nil) as Array
let theText : AnyObject = executed.valueForKey("content")
self.content.text = theText
That has nothing to do with encoding problems.
executeFetchRequest() returns an array of managed objects.
Applying valueForKey() to that array returns an array of values.
Printing the description of an array uses \Unnnn escape sequences for all
non-ASCII characters.
So the solution should be simple: Select a single element of the fetched array:
let executed = context.executeFetchRequest(theReq, error: nil)[0] as NSManagedObject
let theText = executed.valueForKey("content") as String
self.content.text = theText
Of course should also check if the fetch was successful or failed, and if it returned
any object. A more detailed version would look like this:
var error : NSError?
let result = context.executeFetchRequest(theReq, error: &error)
if !result {
println("fetch failed: \(error)")
} else if result.count == 0 {
println("nothing found")
} else {
let obj = result[0] as NSManagedObject
let theText = obj.valueForKey("timeStamp") as String
self.content.text = theText
}