How do I replace a placeholder text with an Image in SwiftUI? - swift

I have a string "Hello {world}" which I need to replace with "Hello 🌍". The placeholder's position is not fixed at the end. And I may have more than a single placeholder.
I am using SwiftUI and tried to make this work with
Text("Hello {world}".replacingOccurrences(of: "{world}", with: "\(Image(systemName: "globe"))"))
but soon found that this doesn't work and presented with this Hello Image(provider: SwiftUI.ImageProviderBox<SwiftUI.Image.(unknown context at $1ba606db0).NamedImageProvider>)
Since this worked
Text(LocalizedStringKey("Hello \(Image(systemName: "globe"))"))
I assumed I needed to pass a LocalizedStringKey into the Text I tried again with
Text(LocalizedStringKey("Hello {world}".replacingOccurrences(of: "{world}", with: "\(Image(systemName: "globe"))")))
Text(LocalizedStringKey("Hello" + "\(Image(systemName: "globe"))")) //this doesn't work either
but presented with a similar issue SwiftUI.Text.Storage.anyTextStorage(SwiftUI.(unknown context at $1ba668448).LocalizedTextStorage
I looked at the API for LocalizedStringKey and LocalizedStringKey.StringInterpolation but could not find a solution this problem. Is there a way to make replacement of placeholder string work?

After looking at #bewithyou's answer, I got the idea that I need to split this into multiple substrings and recombine the texts individually. This is the best solution I could come up with:
public extension String {
func componentsKeepingSeparator(separatedBy separator: Self) -> Array<String> {
self.components(separatedBy: separator)
.flatMap { [$0, separator] }
.dropLast()
.filter { $0 != "" }
}
}
And on playground, if I were to run this, it works perfectly.
PlaygroundPage.current.setLiveView(
"Hello {world}!"
.componentsKeepingSeparator(separatedBy: "{world}")
.reduce(Text("")) { text, str in
if str == "{world}" { return text + Text("\(Image(systemName: "globe"))") }
return text + Text(str)
}
)
I'm sure there is a more optimal solution, but this will do for now.
EDIT:
Since I needed support for multiple placeholders, I've added some more extensions that does the job more comprehensively.
func componentsKeepingSeparators(separatedBy separators: [Self]) -> [String] {
var finalResult = [self]
separators.forEach { separator in
finalResult = finalResult.flatMap { strElement in
strElement.componentsKeepingSeparator(separatedBy: separator)
}
}
return finalResult
}
and on the playground
PlaygroundPage.current.setLiveView(
"Hello {world}{world}{world}! {wave}"
.componentsKeepingSeparators(separatedBy: ["{world}", "{wave}"])
.reduce(Text("")) { text, str in
if str == "{world}" { return text + Text("\(Image(systemName: "globe"))") }
if str == "{wave}" { return text + Text("\(Image(systemName: "hand.wave"))") }
return text + Text(str)
}
)
This extension has a double loop and might not be very efficient, so again, if someone can think of a better solution, please do post.

I came to this question via answering this one and it piqued my interest. As I say in my answer there, the secret sauce is that LocalizedStringKey, when initialised with an interpolated string literal, is capable of building in references to SwiftUI Image types which can be rendered in Text.
Because you're not using an interpolated string literal, you can either build things up by multiple Texts, as in the other answers here, or do something smart with LocalizedStringKey.StringInterpolation. The advantage of this approach is that you can also use the image-holding text in any other view that uses LocalizedStringKey (which is, well, pretty much any of them that display text).
This extension on LocalizedStringKey will manually build an interpolated string:
extension LocalizedStringKey {
private static let imageMap: [String: String] = [
"world": "globe",
"moon": "moon"
]
init(imageText: String) {
var components = [Any]()
var length = 0
let scanner = Scanner(string: imageText)
scanner.charactersToBeSkipped = nil
while scanner.isAtEnd == false {
let up = scanner.scanUpToString("{")
let start = scanner.scanString("{")
let name = scanner.scanUpToString("}")
let end = scanner.scanString("}")
if let up = up {
components.append(up)
length += up.count
}
if let name = name {
if start != nil, end != nil, let imageName = Self.imageMap[name] {
components.append(Image(systemName: imageName))
length += 1
} else {
components.append(name)
}
}
}
var interp = LocalizedStringKey.StringInterpolation(literalCapacity: length, interpolationCount: components.count)
for component in components {
if let string = component as? String {
interp.appendInterpolation(string)
}
if let image = component as? Image {
interp.appendInterpolation(image)
}
}
self.init(stringInterpolation: interp)
}
}
You may want to cache these values if they are coming from an API, I haven't checked the performance of this code in a rendering loop.
You add an extension on Text, or any other view:
extension Text {
init(imageText: String) {
self.init(LocalizedStringKey(imageText: imageText))
}
}
So you can do this:
Text(imageText: "Hello {world}! or {moon} or {unmapped}")
Which gives you:

For your question the key here is not LocalizedStringKey but the key here is \() methods means string interpolation.
According to Swift document, string interpolation is a way to construct a new String value from a mix of constants, variables, literals, and expressions by including their values inside a string literal. You can use string interpolation in both single-line and multiline string literals.
In here it combines two things which is Text("hello") and Image(systemName: "globe") into a new String. Your code is wrong because of you append the string of value.
Without LocalizedStringKey, Text will appear as same as your Hello 🌍!.
Text("Hello \(Image(systemName: "globe"))!")
Or you can use as combination for easier understanding
Text("hello") + Text(Image(systemName: "globe")) + Text("!")
And for you question about mapping value you can make a dictionary for mapping image or name image do that
var dict : [String:String] = ["world" : "globe"]
// Add default name image value if key is nil
Text("Hello \(Image(systemName: dict["world", default:"globe"]))!")
Text("hello") + Text(Image(systemName: dict["world", default: "globe"])) + Text("!")
var dict : [String:Image] = ["world" : Image(systemName: "globe")]
// Add default image value if key is nil
Text("hello\(dict["world", default: Image(systemName: "globe")])!")
Text("hello") + Text(dict["world", default: Image(systemName: "globe")]) + Text("!")
All of them works the same an print out Hello 🌍!

Using #Aswath's answer, here's a custom container:
struct CText: View {
var text: String
var placeholders: [String: String]
var imagePlaceholders: [String: Image]
public init(_ text: String) {
self.text = text
self.placeholders = [:]
self.imagePlaceholders = [:]
}
private init(_ text: String, placeholders: [String: String], imagePlaceholders: [String: Image]) {
self.text = text
self.placeholders = placeholders
self.imagePlaceholders = imagePlaceholders
}
private var array: [String] {
let strings = Array(placeholders.keys)
let images = Array(imagePlaceholders.keys)
return strings + images
}
var body: Text {
text
.componentsKeepingSeparators(separatedBy: array)
.reduce(Text("")) { text, str in
if let place = placeholders[str] {
return text + Text(place)
}else if let place = imagePlaceholders[str] {
return text + Text("\(place)")
} else {
return text + Text(str)
}
}
}
func replacing(_ holder: String, with replacement: String) -> CText {
var oldPlaceholders = placeholders
oldPlaceholders[holder] = replacement
return CText(text, placeholders: placeholders, imagePlaceholders: imagePlaceholders)
}
func replacing(_ holder: String, with replacement: Image) -> CText {
var oldPlaceholders = imagePlaceholders
oldPlaceholders[holder] = replacement
return CText(text, placeholders: placeholders, imagePlaceholders: oldPlaceholders)
}
}
Usage:
struct Test: View {
var body: some View {
CText("Hello {world}")
.replacing("{world}", with: Image(systemName: "globe"))
}
}
Edit: If you need to access Text instead of View, add .body at the end:
struct Test: View {
var body: some View {
CText("Hello {world}")
.replacing("{world}", with: Image(systemName: "globe"))
.body
}
}

This is a small improvement to #jrturton's answer tweaked to my needs. Perhaps this might benefit others. However, this is very different to my original answer, and so it made sense to me to add this as a new answer. The deletingPrefix is from hackingwithswift
import PlaygroundSupport
import Foundation
import SwiftUI
extension String {
func deletingPrefix(_ prefix: String) -> String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
}
}
extension LocalizedStringKey {
#available(iOS 15, *)
init(imageText: String, replacementClosure: (String) -> Any) {
var components = [Any]()
var length = 0
let scanner = Scanner(string: imageText)
scanner.charactersToBeSkipped = nil
while scanner.isAtEnd == false {
let up = scanner.scanUpToString("{")
let start = scanner.scanString("{")
let name = scanner.scanUpToString("}")
let end = scanner.scanString("}")
if let up = up {
components.append(up)
length += up.count
}
if let name = name {
if start == nil || end == nil { self.init(stringLiteral: imageText) }
let replacement = replacementClosure(name)
switch replacement {
case let image as Image:
components.append(image)
case let attributedString as AttributedString:
components.append(attributedString)
case let plainString as String:
components.append(plainString)
default:
print("No action.")
}
}
}
var interp = LocalizedStringKey.StringInterpolation(literalCapacity: length, interpolationCount: components.count)
for component in components {
if let string = component as? String {
interp.appendInterpolation(string)
}
if let attrString = component as? AttributedString {
interp.appendInterpolation(attrString)
}
if let image = component as? Image {
interp.appendInterpolation(image)
}
}
self.init(stringInterpolation: interp)
}
}
extension Text {
init(imageText: String) {
self.init(LocalizedStringKey(imageText: imageText, replacementClosure: { string in
switch string {
case "world":
return Image(systemName: "globe")
case "moon":
return Image(systemName: "moon")
case let stylisedString where stylisedString.hasPrefix("style1__"):
return AttributedString(stylisedString.deletingPrefix("style1__"), attributes: AttributeContainer().foregroundColor(.blue))
default: return string
}
}))
}
}
PlaygroundPage.current.setLiveView(Text(imageText: "Hello {world}! or {moon} or {style1__unmapped}")
)

Related

How to cut last characters of string and save it as variable in swift?

I have a string of test#me
now I want to create a code that let me cut where the # is and save it as a variable, so the expected result is string1 = test string2 = #me
a code will be something like this
func test(string: String) {
var mString = string
var cuttedString = mString.cutFrom("#")
print(mString)
print(cuttedString)
}
test(string: "test#me")
result:
test
#me
Here is an extension of String which performs the function you desire. It takes a String and mutates the calling String by removing everything from that part on, and it returns the part that was removed.
import Foundation
extension String {
mutating func cut(from string: String) -> String {
if let range = self.range(of: string) {
let cutPart = String(self[range.lowerBound...])
self.removeSubrange(range.lowerBound...)
return cutPart
}
else {
return ""
}
}
}
func test(string: String) {
var mString = string
let cutString = mString.cut(from: "#")
print(mString)
print(cutString)
}
test(string: "test#me")
test
#me
A Generic Implementation
Here is a generic implementation suggested by #LeoDabus in the comments:
extension StringProtocol where Self: RangeReplaceableCollection {
#discardableResult mutating func removeSubrange<S: StringProtocol>(from string: S) -> SubSequence {
guard let range = range(of: string) else { return "" }
defer { removeSubrange(range.lowerBound...) }
return self[range.lowerBound...]
}
}
It nicely demonstrates:
Extending a protocol instead of String to allow the function to be used with other types such as String.Subsequence.
The use of #discardableResult which allows the function to be called to shorten the String without using the substring that is returned.
Using a guard let statement to unwrap the optional return from range(of:) and provide an early exit if the range is nil.
The use of defer to delay the removal of the substring until after the substring has been returned which avoids the use a local variable.
Use suffix(from:) in combination with firstIndex(of:)
let string = "test#me"
let last = string.suffix(from: string.firstIndex(of: "#") ?? string.startIndex)
Note that this returns the full string if "#" is not found, you could instead return an empty string by replacing string.startIndex with string.endIndex
To split the string in two parts, ie "test" and "#me"
var first: String = ""
var last: String = ""
if let index = string.firstIndex(of: "#") {
last = String(string.suffix(from: index))
first = String(string.prefix(upTo: index))
}
print(first, last)
You can use String subscript for this:
func test(string: String) {
guard let atIndex = string.firstIndex(of: "#") else { return }
let mString = string[string.startIndex...string.index(before: atIndex)]
let cuttedString = string[atIndex..<string.endIndex]
print(mString)
print(cuttedString)
}
func test(string: String) {
let mString = string.prefix { $0 != "#" }
let cuttedString = string.suffix(from: string.firstIndex(of: "#") ?? string.startIndex)
print(mString)
print(cuttedString)
}
test(string: "test#me")
// prints test
// #me
But remember that mString and cuttedString are not String but Substring, so take care of correct usage.
There are various ways to do this.
You could use components(separatedBy:) to break the string into pieces, and then add an # back to the last one:
extension String {
func cutFrom(_ divider: Character) -> String {
let array = components(separatedBy: String(divider))
//For an Optional, `map()` returns nil if the optional is empty,
//Or the result of applying the closure to the unwrapped contents if not
return array.last.map {String(divider) + $0} ?? ""
}
}
Alternately, you could use firstIndex to find the index of the first divider character in the string, and then return a substring from that index to the end:
extension String {
func cutFrom(_ divider: Character) -> String {
//For an Optional, `map()` returns nil if the optional is empty,
//Or the result of applying the closure to the unwrapped contents if not
return firstIndex(of: divider).map { String(self[$0..<endIndex])} ?? ""
}
}
It might be cleaner to have both versions of the function return Optionals, and return NIL if the divider character can't be found. You also might want to adjust the logic to deal with strings where the divider character occurs more than once.

Highlight a specific part of the text in SwiftUI

Hello I'm new to Swift and am using SwiftUI for my project where I download some weather data and I display it in the ContentView().
I would like to highlight some part of the Text if it contains some specific word, but I don't have any idea how to start.
In ContentView(), I have tried to set a function receiving the string downloaded from web and return a string. I believe this is wrong, because SwiftUI does not apply the modifiers at the all for the Text.
For example, in my ContentView() I would like the word thunderstorm to have the .bold() modifier:
struct ContentView: View {
let testo : String = "There is a thunderstorm in the area"
var body: some View {
Text(highlight(str: testo))
}
func highlight(str: String) -> String {
let textToSearch = "thunderstorm"
var result = ""
if str.contains(textToSearch) {
let index = str.startIndex
result = String( str[index])
}
return result
}
}
If that requires just simple word styling then here is possible solution.
Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
let testo : String = "There is a thunderstorm in the area. Added some testing long text to demo that wrapping works correctly!"
var body: some View {
hilightedText(str: testo, searched: "thunderstorm")
.multilineTextAlignment(.leading)
}
func hilightedText(str: String, searched: String) -> Text {
guard !str.isEmpty && !searched.isEmpty else { return Text(str) }
var result: Text!
let parts = str.components(separatedBy: searched)
for i in parts.indices {
result = (result == nil ? Text(parts[i]) : result + Text(parts[i]))
if i != parts.count - 1 {
result = result + Text(searched).bold()
}
}
return result ?? Text(str)
}
}
Note: below is previously used function, but as commented by #Lkabo it has limitations on very long strings
func hilightedText(str: String) -> Text {
let textToSearch = "thunderstorm"
var result: Text!
for word in str.split(separator: " ") {
var text = Text(word)
if word == textToSearch {
text = text.bold()
}
result = (result == nil ? text : result + Text(" ") + text)
}
return result ?? Text(str)
}
iOS 13, Swift 5. There is a generic solution described in this medium article. Using it you can highlight any text anywhere with the only catch being it cannot be more then 64 characters in length, since it using bitwise masks.
https://medium.com/#marklucking/an-interesting-challenge-with-swiftui-9ebb26e77376
This is the basic code in the article.
ForEach((0 ..< letter.count), id: \.self) { column in
Text(letter[column])
.foregroundColor(colorCode(gate: Int(self.gate), no: column) ? Color.black: Color.red)
.font(Fonts.futuraCondensedMedium(size: fontSize))
}
And this one to mask the text...
func colorCode(gate:Int, no:Int) -> Bool {
let bgr = String(gate, radix:2).pad(with: "0", toLength: 16)
let bcr = String(no, radix:2).pad(with: "0", toLength: 16)
let binaryColumn = 1 << no - 1
let value = UInt64(gate) & UInt64(binaryColumn)
let vr = String(value, radix:2).pad(with: "0", toLength: 16)
print("bg ",bgr," bc ",bcr,vr)
return value > 0 ? true:false
}
You can concatenate with multiple Text Views.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View{
let testo : String = "There is a thunderstorm in the area"
let stringArray = testo.components(separatedBy: " ")
let stringToTextView = stringArray.reduce(Text(""), {
if $1 == "thunderstorm" {
return $0 + Text($1).bold() + Text(" ")
} else {
return $0 + Text($1) + Text(" ")
}
})
return stringToTextView
}
}
PlaygroundPage.current.setLiveView(ContentView())
If you are targeting iOS15 / macOS12 and above, you can use AttributedString. For example:
private struct HighlightedText: View {
let text: String
let highlighted: String
var body: some View {
Text(attributedString)
}
private var attributedString: AttributedString {
var attributedString = AttributedString(text)
if let range = attributedString.range(of: highlighted)) {
attributedString[range].backgroundColor = .yellow
}
return attributedString
}
}
If you want your match to be case insensitive, you could replace the line
if let range = attributedString.range(of: highlighted)
with
if let range = AttributedString(text.lowercased()).range(of: highlighted.lowercased())
The answer of #Asperi works well. Here is a modified variant with a search by array of single words:
func highlightedText(str: String, searched: [String]) -> Text {
guard !str.isEmpty && !searched.isEmpty else { return Text(str) }
var result: Text!
let parts = str.components(separatedBy: " ")
for part_index in parts.indices {
result = (result == nil ? Text("") : result + Text(" "))
if searched.contains(parts[part_index].trimmingCharacters(in: .punctuationCharacters)) {
result = result + Text(parts[part_index])
.bold()
.foregroundColor(.red)
}
else {
result = result + Text(parts[part_index])
}
}
return result ?? Text(str)
}
Usage example:
let str: String = "There is a thunderstorm in the area. Added some testing long text to demo that wrapping works correctly!"
let searched: [String] = ["thunderstorm", "wrapping"]
highlightedText(str: str, searched: searched)
.padding()
.background(.yellow)
You can also make AttributedString with markdown this way
do {
return try AttributedString(markdown: foreignSentence.replacing(word.foreign, with: "**\(word.foreign)**"))
} catch {
return AttributedString(foreignSentence)
}
and just use Text
Text(foreignSentenceMarkdown)

Grey Matcher to get text of multiple elements matches the same grey matcher

I am new to this framework. Could you please help me to get the text of multiple elements matches the same matcher on UI.
You can get the text of the element using the following function
open class GreyElement {
var text = ""
}
func grey_getText(_ elementCopy: GreyElement) -> GREYActionBlock {
return GREYActionBlock.action(withName: "get text",
constraints: grey_respondsToSelector(#selector(getter: UILabel.text))) { element,
errorOrNil -> Bool in
let elementObject = element as? NSObject
let text = elementObject?.perform(#selector(getter: UILabel.text),
with: nil)?.takeRetainedValue() as? String
elementCopy.text = text ?? ""
return true
}
}
And then in your test code:
var label = GreyElement()
for i in 0..<100 {
EarlGrey.selectElement(...).perform(grey_getText(text))
XCTAssert(label.count > 10)
}
The XCTest version:
for element in app.staticText[...].allElementsBoundByIndex {
XCTAssert(element.label.count > 10)
}

Swift: assign to variable in switch case-let-where

Is it possible to make an assignment to the artist variable before it is used in the where subclause?
var artist
switch fullline {
case let path where path.hasPrefix("Monet"):
artist = "Monet"
case let path where path.hasPrefix("Cezanne"):
artist = "Cezanne"
default: ()
}
Closure:
case let path where { () -> Bool in let artist = "Monet"; return path.hasPrefix(artist) }:
Error:
() -> Bool' is not convertible to 'Bool'
Context:
I have lines of freeform text with artist name as the prefix that requires
massaging to output consistent humanly readable text. e.g.
Monet : Snow at Argenteuil 02, 1874
Monet - Snow at Argenteuil, 1874, 3rd Floor Collections
Monet, Claude - 1875, Snow in Argenteuil
Cezzane - Vase of Flowers, 1880-81, print
Cezzane, Paul 1900-1903 Vase of Flowers
Cezzane - Vase with Flowers, 1895-1896
There will be a code fragments that performs detailed processing/categorizing
for each artist. Hence the processing logic is artist dependent.
I would like to define similar to the following construct
switch fullline
hasPrefix(artist = "Monet")
-> code logic 1
get_birthday(artist)
hasPrefix(artist = "Cezzane")
-> code logic 2
get_birthday(artist)
With a little modification to the Alexander's struct, you can write something like this:
struct PrefixMatcherWithHandler {
var handler: (String)->Void
var string: String
init(_ string: String, handler: #escaping (String)->Void) {
self.string = string
self.handler = handler
}
static func ~= (prefix: String, matcher: PrefixMatcherWithHandler) -> Bool {
if matcher.string.hasPrefix(prefix) {
matcher.handler(prefix)
return true
} else {
return false
}
}
}
var fullline: String = "Monet, Claude"
var artist: String? = nil
let matcher = PrefixMatcherWithHandler(fullline) {str in
artist = str
}
switch matcher {
case "Monet":
break
case "Cezanne":
break
default: break
}
print(artist ?? "") //->Monet
But having some side-effect in boolean operators like ~= makes your code less readable and can easily make unexpected result.
If you just want to reduce some redundant reference to a same thing, switch-statement may not be a good tool for it.
For example, you can get the same result without defining specific matcher types:
var fullline: String = "Monet, Claude"
var artist: String? = nil
if let match = ["Monet", "Cezanne"].first(where: {fullline.hasPrefix($0)}) {
artist = match
}
print(artist ?? "") //->Monet
ADDED for updated parts of the question
The following code behaves slightly different than prefix-matching, but I believe you do not want to match "Mon" to the line Monet, Claude - 1875, Snow in Argenteuil.
extension String {
var firstWord: String? {
var result: String? = nil
enumerateSubstrings(in: startIndex..<endIndex, options: .byWords) {str, _, _, stop in
result = str
stop = true
}
return result
}
}
func get_birthday(_ artist: String) {
//What do you want to do?
print(artist)
}
var fullline: String = "Monet, Claude - 1875, Snow in Argenteuil"
switch fullline.firstWord {
case let artist? where artist == "Monet":
//code dedicated for "Monet"
get_birthday(artist)
case let artist? where artist == "Cezanne":
//code dedicated for "Cezanne"
get_birthday(artist)
default:
break
}
When you can retrieve data suitable for switch-statement, the code would be far more intuitive and readable.
You're giving that closure where a boolean is expected. Not sure why you would want to do this, but you could make it work by using () to invoke the closure.
var artist
switch fullline {
case let path where { () -> Bool in let artist = "Monet"; return path.hasPrefix(artist) }():
artist = "Monet"
case let path where path.hasPrefix("Cezanne"):
artist = "Cezanne"
default: ()
}
Here is how I would do this:
import Foundation
struct PrefixMatcher {
let string: String
init(_ string: String) { self.string = string }
static func ~= (prefix: String, matcher: PrefixMatcher) -> Bool {
return matcher.string.hasPrefix(prefix)
}
}
extension String {
var prefix: PrefixMatcher { return PrefixMatcher(self) }
}
let fullline = "Monet 123456789"
let artist: String?
switch fullline.prefix {
case "Monet": artist = "Monet"
case "Cezanne": artist = "Cezanne"
default: artist = nil
}
print(artist as Any)
More general solution:
import Foundation
struct PredicateMatcher<Pattern> {
typealias Predicate = (Pattern) -> Bool
let predicate: Predicate
static func ~=(pattern: Pattern,
matcher: PredicateMatcher<Pattern>) -> Bool {
return matcher.predicate(pattern)
}
}
extension String {
var prefix: PredicateMatcher<String> {
return PredicateMatcher(predicate: self.hasPrefix)
}
}
You can achieve this by switching over a tuple of your enum and your optional.
Optional is an enum too, so you can switch both of them
enum SomeSnum {
case a, b, c
}
let someString: String? = "something"
let esomeEnum = SomeSnum.b
switch(esomeEnum, someString) {
case (.b, .some(let unwrappedSomething)) where unwrappedSomething.hasPrefix("so"):
print("case .b, \(unwrappedSomething) is unwrapped, and it has `so` prefix")
case (.a, .none):
print("case .a, and optional is nil")
default:
print("Something else")
}
You can also do an if statement
if case let (.b, .some(unwrappedSomething)) = (esomeEnum, someString), unwrappedSomething.hasPrefix("so") {
} else if case (.a, .none) = (esomeEnum, someString) {
} else {
}

How to remove duplicate characters from a string in Swift

ruby has the function string.squeeze, but I can't seem to find a swift equivalent.
For example I want to turn bookkeeper -> bokepr
Is my only option to create a set of the characters and then pull the characters from the set back to a string?
Is there a better way to do this?
Edit/update: Swift 4.2 or later
You can use a set to filter your duplicated characters:
let str = "bookkeeper"
var set = Set<Character>()
let squeezed = str.filter{ set.insert($0).inserted }
print(squeezed) // "bokepr"
Or as an extension on RangeReplaceableCollection which will also extend String and Substrings as well:
extension RangeReplaceableCollection where Element: Hashable {
var squeezed: Self {
var set = Set<Element>()
return filter{ set.insert($0).inserted }
}
}
let str = "bookkeeper"
print(str.squeezed) // "bokepr"
print(str[...].squeezed) // "bokepr"
I would use this piece of code from another answer of mine, which removes all duplicates of a sequence (keeping only the first occurrence of each), while maintaining order.
extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
var alreadyAdded = Set<Iterator.Element>()
return self.filter { alreadyAdded.insert($0).inserted }
}
}
I would then wrap it with some logic which turns a String into a sequence (by getting its characters), unqiue's it, and then restores that result back into a string:
extension String {
func uniqueCharacters() -> String {
return String(self.characters.unique())
}
}
print("bookkeeper".uniqueCharacters()) // => "bokepr"
Here is a solution I found online, however I don't think it is optimal.
func removeDuplicateLetters(_ s: String) -> String {
if s.characters.count == 0 {
return ""
}
let aNum = Int("a".unicodeScalars.filter{$0.isASCII}.map{$0.value}.first!)
let characters = Array(s.lowercased().characters)
var counts = [Int](repeatElement(0, count: 26))
var visited = [Bool](repeatElement(false, count: 26))
var stack = [Character]()
var i = 0
for character in characters {
if let num = asciiValueOfCharacter(character) {
counts[num - aNum] += 1
}
}
for character in characters {
if let num = asciiValueOfCharacter(character) {
i = num - aNum
counts[i] -= 1
if visited[i] {
continue
}
while !stack.isEmpty, let peekNum = asciiValueOfCharacter(stack.last!), num < peekNum && counts[peekNum - aNum] != 0 {
visited[peekNum - aNum] = false
stack.removeLast()
}
stack.append(character)
visited[i] = true
}
}
return String(stack)
}
func asciiValueOfCharacter(_ character: Character) -> Int? {
let value = String(character).unicodeScalars.filter{$0.isASCII}.first?.value ?? 0
return Int(value)
}
Here is one way to do this using reduce(),
let newChar = str.characters.reduce("") { partial, char in
guard let _ = partial.range(of: String(char)) else {
return partial.appending(String(char))
}
return partial
}
As suggested by Leo, here is a bit shorter version of the same approach,
let newChar = str.characters.reduce("") { $0.range(of: String($1)) == nil ? $0.appending(String($1)) : $0 }
Just Another solution
let str = "Bookeeper"
let newChar = str.reduce("" , {
if $0.contains($1) {
return "\($0)"
} else {
return "\($0)\($1)"
}
})
print(str.replacingOccurrences(of: " ", with: ""))
Use filter and contains to remove duplicate values
let str = "bookkeeper"
let result = str.filter{!result.contains($0)}
print(result) //bokepr