Since String and NSString can be converted freely, is there something to notice when using it, or which one is better?

Frankly speaking, both of them are excellent. However, if you insist to choose one, I suggest picking up Swift native String.

There are 3 reasons for it.

First of all, although we can convert String and NSString seamlessly, all APIs in Swift accept String and return String type when they need strings. There is no requirement for us to do any conversion between them when developing an app. With all the convenient methods on String, we can play well with native String type.

Secondly, in Swift String is a struct, while in Objective-C, NSString is a class and inherit from NSObject. In concept, Swift String is more likely immutable. If we use a struct type and constant (remember the let keyword), we can keep out a lot of consideration of multi-thread programming and make us a better life. Besides of that, if we do not touch anything specified on NSString or runtime feature, we will get some performance improvement by persisting in String.

At last, thanks to implement of some protocols, such as CollectionType, we can use some great Swift syntax on String only. A good example might be using for...in in a string. We could write:

let levels = "ABCDE"
for i in levels {
    print(i)
}

// Output:
// ABCDE

Once we convert levels to NSString, it will not compile at all.

But every coin has two sides. Some methods of NSString are also absent from String. Take an example, containsString is a new added API of NSString in iOS 8. But you cannot find it in the ported module of Foundation. So if you want to use this API, you have to do a conversion to NSString first:

if (levels as NSString).containsString("BC") {
    print("Containing")
}

// Output:
// Containing

The lack of containsString in String is an odd thing. I guess Apple just forget to make it available in String. Of course, you can add your own extension for it if you use it frequently.

Some other APIs, such as length and characterAtIndex: also absent in String. But it mainly due to the difference between two kinds of encoding handling in String and NSString.

The hardest thing when using a String would be using it with a Range. In NSString, when we want to match some string, we always use an NSRange to indicate the result or as a parameter. However, the corresponding APIs in Swift are accepting a particular version of range: Range<String.Index>. Sometimes, it becomes very annoying, because NSRange cannot be converted to Range<String.Index>:

let levels = "ABCDE"

let nsRange = NSMakeRange(1, 4)
// Compile Error!
// 'NSRange' is not convertible to 'Range<String.Index>'
levels.stringByReplacingCharactersInRange(nsRange, withString: "AAAA")

// Instead, we have to do like this:
let indexPositionOne = levels.startIndex.successor()
let swiftRange = indexPositionOne..<advance(indexPositionOne, 4)
levels.stringByReplacingCharactersInRange(swiftRange, withString: "AAAA")
// Output;
// AAAAA

It is not fun at all to write such swiftRange thing. We like to work with Int and NSRange, which is much easier than Range<String.Index>. In this case, conversion to NSString would be a better choice.

let nsRange = NSMakeRange(1, 4)
(levels as NSString).stringByReplacingCharactersInRange(
    nsRange, withString: "AAAA")