Substring?

Hi,


I found a Swift example containing this expression:


attributes.substring(from: 1, to: 2)


When I type this into Xcode it comes up as a syntax error.


I guess this is something they changed in Swift between 3 and 4, or 2 and 3, or something.


What is the correct way to do this now?

Replies

Use a range operator:


attributes[1 ..< 2]


or possibly:


attributes[1 ... 2]


depending on whether you want to exclude element 2 (..<) or include it (...).

I tried that. It didn't work. It says the indexes need to be of some type other than a number.

Oh, so sorry, I wasn't paying proper attention. The way to do exactly this for Strings is ugly:


let string = "abcd"
let firstIndex = string.index(string.startIndex, offsetBy: 1)
let otherIndex = string.index(string.startIndex, offsetBy: 2)
print(string [firstIndex ..< otherIndex]) // prints "b"
print(string [firstIndex ... otherIndex]) // prints "bc"


A revamp of String APIs for usability is on the radar for a future Swift version, but for now it's this clunky.


Note, though, that in most cases you won't actually want to index directly into a String with an Int. (What's the use case, for example, of getting the 3rd character of someone's name?) If you approach the larger problem differently, you might work more naturally in the String.Index type, and it's not so bad.


You can also do something like this:


let substring = string.dropFirst(1).prefix(2)
print(substring) // prints "bc"


Note also that some of these APIs will produce a substring type. If you want to "promote" it back to a String, you'll write something like:


let substring = String(string.dropFirst(1).prefix(2))


Hope that helps!

Thanks, I think I understand it now. I ended up with this expression:


attributes[attributes.index(attributes.startIndex, offsetBy: 1)..<attributes.index(attributes.startIndex, offsetBy: 2)]

In retrospect I feel silly for not thinking of that, it's so obvious.


Frank

In retrospect I feel silly for not thinking of that, it's so obvious.

I’m not sure if that was intended to be sarcastic or not. Either way, I encourage you to rethink your tone. Everyone here is genuinely trying to help.

Let’s take a step back here. Why are you trying to extract the second character from a string? If the string holds localised content, getting the second character is rarely useful. If you have a specific use case where that makes sense, I’d love to hear it.

The other common case I’ve seen for folks asking questions like this is that they’re using

String
to represent something that’s not user-visible, like an element of a network protocol. In such situations it may make more sense to use a different data type. Or build a custom abstraction on top of
String
.

If you can explain more of the backstory here, I’d be happy to offer further advice.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the reply. Yes, I think it is silly that you need to do such a convoluted thing to extract a substring. I meant no disrespect by my comment. I appreciate everyone's willingness to help.


As for why I want to do this, the short answer is I was copying some example code I found into my project and that's what was in the example. The string in question came from a call to the property_getAttributes function in the Objective-C runtime. It looks like the author of the original code was trying to ascertain the data type of the property by looking at that character ("c" means Int8, "s" means Int16, etc).


Frank

The string in question came from a call to the

property_getAttributes
function in the Objective-C runtime.

Right, this is a classic example of the second case I mentioned, where folks are using a

String
to hold some data structure that isn’t really text. In that case I often avoid this problem entirely by working with arrays of bytes. For example [1]:
let prop: objc_property_t = …
let attr = property_getAttributes(prop)!
if attr[1] == CChar(ascii: "s") {
    … do something …
}

In this case you know that

attr
is an ASCII C string, and thus you can index it as an array.

Of course this is unsafe because you’re working with pointers, just like you are in Objective-C. If you’re doing this a few times, you can create simple wrapper functions for the cases you need; those will prevent the unsafeness from leaking out into the rest of your app.

OTOH, if you’re doing a tonne of work with the Objective-C runtime, you can build an abstraction that presents this information in a way that’s more digestable to the rest of your app.

One of the nice things about Swift is that these abstractions can be very lightweight. In Objective-C your primary abstraction mechanism is objects, and those are relatively heavyweight. In Swift you have powerful structs, that allow you to build abstractions that are easy for your clients to use and yet cheap at runtime.

For example, the following type is no more expensive than a raw

objc_property_t
type:
struct QObjCProperty {
    init(property: objc_property_t) {
        self.property = property
    }
    let property: objc_property_t
}

And yet it allows you to add abstractions like this one:

extension QObjCProperty {
    var isInt16: Bool {
        guard let attr = property_getAttributes(self.property) else { return false }
        return attr[1] == CChar(ascii: "a")
    }
}

WARNING It’s been a long time since I’ve worked with Objective-C property attribute strings, so the logic I’m using in

isInt16
might be completely bogus. The point of this code is to show how to build the abstraction, not as an example of the correct way to use the Objective-C runtime.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] This code doesn’t actually compile out of the box because

CChar
doesn’t have an
init(ascii:)
initialiser (it’s present for
UInt8
but not
Int8
). To get around this I added my own initialiser in an extension.
extension CChar {
    init(ascii: Unicode.Scalar) {
        precondition(ascii.isASCII)
        self = Int8(ascii.value)
    }
}