The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”

41. The singleton

Singletons are a common pattern in Cocoa. Singletons are used to store and access instances that are expected to be globally accessible, or objects that should only exist in one instance during the lifetime of the app. The accepted way to write singletons in Objective-C is something like this:

@implementation MyManager
+ (id)sharedManager {
    static MyManager * staticInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticInstance = [[self alloc] init];
});
    return staticInstance;
}
@end
Copy the code

Using dispatch_once_t in GCD ensures that the code inside is called only once, thus ensuring that the singleton is thread safe.

Since GCD is seamless in Swift, it is very easy to rewrite singletons in a similar manner to Swift. (Since Apple removed dispatch_once in Swift 3, this only works for versions prior to Swift 3. And it’s not the best way, we’ll see a better implementation):

class MyManager {
    class var sharedManager : MyManager {
        struct Static {
            static var onceToken : dispatch_once_t = 0
            static var staticInstance : MyManager? = nil
        }
        dispatch_once(&Static.onceToken) {
            Static.staticInstance = MyManager()}return Static.staticInstance!}}Copy the code

This is fine, of course, but there is a simpler thread-safe way in Swift, which is called let. To simplify this, we can write:

class MyManager {
    class var shared : MyManager {
        struct Static {
            static let sharedInstance : MyManager = MyManager()}return Static.sharedInstance
    }
}
Copy the code

There is another approach that is more popular and considered a best practice prior to Swift 1.2. Since classes did not support stored properties prior to Swift 1.2, when you wanted to use a property that only existed in one copy, you had to define it in a global scope. Fortunately, there is access level control in Swift. You can prefix a variable definition with the private keyword to make it accessible only in the current file. This allows you to write a singleton that is not nested, but is syntactically simpler and prettier:

private let sharedInstance = MyManager(a)class MyManager  {
    class var shared : MyManager {
        return sharedInstance
    }
}
Copy the code

Improvements in Swift 1.2: Storage class variables such as static let and static var were not supported before Swift 1.2. But in 1.2 Swift added support for class variables, so singletons can be further simplified. Add the above global sharedInstance to the class to make it more compact and reasonable.

In Swift 1.2 and beyond, if there are no specific requirements, it is recommended to write a singleton like this:

 class MyManager  {
    static let shared = MyManager(a)private init(a){}}Copy the code

This writing method is not only concise, but also ensures the uniqueness of singletons. When initializing a class variable, Apple wraps the initialization in a Swift_ONce_Block_invoke to keep it unique. Not only that, but for all global variables, Apple uses a dispatch_once style underneath to ensure that they are initialized lazy only once.

In addition, a private initialization method is added to this type to override the default public initialization method, which prevents other parts of the project from generating their own instances of MyManager through init and ensures the uniqueness of the type singleton. If you want a singleton of the default-like form (that is, users of the class can create their own instances), you can remove the private init method.

42. Conditional compilation

In C languages, you can use a compiler conditional branch like #if or #ifdef to control which code needs to be compiled and which does not. Swift has no concept of macro definition, so you cannot use the #ifdef method to check if a symbol is macro defined. However, in order to control the compilation process and content, Swift provides several simple mechanisms to customize the compilation of content on demand.

// The #if compiler tag still exists and the syntax is the same as before:
#if <condition>
#elseif <condition>
#else
#endif
# elseIf and #else are optional.
Copy the code

Condition is not arbitrary. Swift builds several combinations of platforms and architectures to compile different code for different platforms:

methods Optional parameters
os() macOS, iOS, tvOS, watchOS, Linux
arch() x86_64, arm, arm64, i386
swift() >= A version

Some methods and arguments are case sensitive. One possible way to do this is to conditionally compile with TypeAlias if the API for color is unified between iOS and Mac:

#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif
Copy the code

Although Swift now only works on the platforms listed above, OS () optional parameters also include “FreeBSD”, “Windows” and “Android”.

In addition, the parameters of ARCH () need to be explained that arm and ARM64 correspond to the real machine with 32-bit CPU and 64-bit CPU respectively, while for the simulator, Accordingly, the emulators for 32-bit devices and the emulators for 64-bit devices correspond to i386 and X86_64 respectively, which also need to be treated separately.

For example, you need to use the same target to complete the paid version and the free version of the same app, and you want the paid version to perform the function when you click a button. If the free version pops up a prompt, you can use a method like the following:

@IBAction func someButtonPressed(sender: AnyObject!). {
    #if FREE_VERSION
    // Pop up a purchase prompt, navigate to the store, etc
    #else
    // The actual function #endif
}
Copy the code

The FREE_VERSION compilation symbol is used here to represent the free version. To make this work, you need to set it in your project’s Build options. In your project’s Build Settings, go to Swift Compiler-Custom Flags, Add -d FREE_VERSION to Other Swift Flags.

43. Compile flags

In Objective-C, it is common to insert the #param symbol into code to mark the interval of code so that the list of methods organized into blocks can be seen in the Xcode navigation bar. This is useful for quick positioning when there are many single-file methods.

Similarly in Swift, you can add a tag like // MARK: (note the capitalization) to your code where appropriate, followed by a name. Xcode will look for such a comment in your code, and then display the name in the navigation bar as a bold label.

You can also add a hyphen – after the colon, so that the navigation will show another line at this position, separating the parts more clearly.

In addition to // MARK:, Xcode supports several other tags, namely // TODO: and // FIXME:. Unlike MARK, the other two tags not only show the name or description that follows them in the navigation bar, but also show themselves as reminders of unfinished work or areas that need fixing. This gives you an idea of the current file by first looking at the markup in the navigation bar when reading the source code.

Another commonly used compilation flag in Objective-C is # Warning. A #warning flag can be displayed as a clear yellow warning bar in Xcode’s code editor, perfect for telling code maintainers and users that something needs to be noticed. This feature does not exist in the current Swift version.

44. @UIApplicationMain

In C language, the program entry is the main function. For an Objective-C iOS app project, when creating a new entry, Xcode prepares a main.m file that contains the main function:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil.NSStringFromClass([AppDelegate class])); }}Copy the code

UIKit UIApplicationMain method is called. This method initializes an object of UIApplication or a subclass of it with the third argument and starts receiving events (in this case, passing nil, which means using the default UIApplication). The last parameter specifies the AppDelegate class as the application’s delegate, which is used to receive application life-cycle delegate methods like didFinishLaunching or didEnterBackground. Also, although this method is marked to return an int, it does not actually return. It remains in memory until the user or the system forces it to terminate.

The corresponding situation in the Swift project. When you create a Swift iOS app project, there is no main file in any of the files, as in Objective-C, and there is no main function. The only thing related to Main is the @UIApplicationMain tag at the top of the default AppDelegate class declaration.

What this tag does is it takes the labeled class as a delegate to create a UIApplication and launch the whole program. At compile time, the compiler looks for the tagged class and automatically inserts template code like the main function. Try removing @UIApplicationMain: Undefined symbols _main can’t find main. In general, you don’t need to make any changes to this tag, but if you want to use a subclass of UIApplication instead of itself, you do need to “fiddle” with this part of the content.

The Swift app also requires the main function, but by default the @uiApplicationMain helper generates it automatically. As with the MAIN. C or main.m files of the C language, Swift projects can have a special file called main. Swift. In this file, you don’t need to define scopes and can write code directly. The code in this file will be executed as the main function. For example, after removing @UIApplicationMain, add a main.swift file to your project and add code like this:

 UIApplicationMain(Process.argc, Process.unsafeArgv, nil.NSStringFromClass(AppDelegate))
Copy the code

Now compile and run, and there will be no more errors. You can also easily do something to control the entire application behavior by replacing the third argument with your OWN UIApplication subclass. For example, replace the contents of main.swift with:

import UIKit
class MyApplication: UIApplication {
    override func sendEvent(event: UIEvent!). {
        super.sendEvent(event)
        print("Event sent: \(event)"); }}UIApplicationMain(Process.argc, Process.unsafeArgv,
    NSStringFromClass(MyApplication), NSStringFromClass(AppDelegate))
Copy the code

This way, every time an event is sent (such as a button click), you can listen for that event.

45. @ objc and dynamic

Swift’s original intention was to get rid of objective-C’s heavy historical baggage and constraints, but there is no denying that Cocoa frameworks have long been imbued with objective-C’s indelible imprint. Countless third-party libraries are written in Objective-C, and this accumulation is nothing to sneeze at. As a result, Swift had to consider compatibility with Objective-C in its initial release.

Apple has taken the approach of allowing both Swift and Objective-C development in the same project. Objective-c files and Swift files in a project are in two different worlds, and some Bridges need to be added in order for them to connect to each other.

It’s easy to use Objective-C code in Swift first by adding the {product-module-name} -bridge-header. h file and filling it in with the name of the Header file you want to use. To simplify this setup, Xcode even automatically pops up an Objective-C file when it is imported for the first time in the Swift project, asking if it should be created automatically.

But if you want to use Swift’s type in Objective-C, things get a little more complicated. If it comes from an external frame, then the frame is definitely not in the same target as the Objective-C project and needs to be imported into the external Swift Module. This is actually the same as the original Framework using Objective-C, and it doesn’t make much difference for a project whether the external Framework is written by Swift or objective-C. Introduce module:@import MySwiftKit by using @import, new in 2013; The framework written by Swift can then be used normally.

If you want to use the Swift source file from the same project in Objective-C, you can do so by importing the auto-generated header {product-module-name} -swift.h. For example, if the target of your project is called MyApp, you need to say #import “myapp-swift.h” in your Objective-C file.

Objective-c and Swift use two completely different mechanisms at the bottom. Objective-c objects in Cocoa are runtime based and follow KVC (Key-value Coding, Store object information in a dictionary-like manner) and Dynamic Dispatch (the actual implementation of the call is determined at run time). Swift, in its pursuit of performance, does not make these decisions at run time without special needs. That is, members or methods of type Swift are determined at compile time and can be used at run time without a lookup.

Obviously, the problem with this is that if you try to call a pure Swift type using Objective-C code or features, you will fail because you can’t find the runtime information you need. It’s also easy to fix. In Swift type files, you can prefix declarations that need to be exposed to any place objective-C uses (including classes, attributes, methods, etc.) with the @objc modifier. Note that this step only applies to types that do not inherit from NSObject. If Swift writes classes that inherit from NSObject, Swift automatically assigns @objc to all non-private classes and members by default. That is, if you take a subclass of NSObject, you just import the corresponding header file and you can use that class in Objective-C.

Another purpose of the @objc modifier is to highlight the name of a new declared method or variable for Objective-C. Most of the time, though, automatic method names are good enough (such as Swift init(name: String) converts to -initWithName :(NSString *)name), but sometimes you expect objective-C to use a different method name or class name than Swift, such as the Swift class:

classMy class:NSObject {
    func Say "hello"(The name: String) {
        print("Hello,\ (name)"}} my class (). Hello (name:"Xiao Ming")
Copy the code

Objective-c calls cannot be made in Chinese, so @objc must be converted to ASCII to be accessed in Objective-C:

@objc(MyClass) classMy class{
    @objc(greeting:)
    func Say "hello"(The name: String) {
        print("Hello,\ (name)")}}Copy the code

In Objective-C, you can call code like [[MyClass New] greeting:@”XiaoMing”]. In addition, as mentioned above and mentioned in the Selector section, Swift does not automatically append @objc to methods or members marked private, even if they are subclasses of NSObject, to ensure that dynamic distribution is minimized to improve code execution. If we are sure to use the dynamic nature of these content, we need to manually decorate them with @objc.

Note, however, that adding the @objc modifier does not mean that the method or property will be distributed dynamically, and Swift may still optimize it for static invocation. So if you have the same runtime feature as dynamic calls in Objective-C, the modifier you need to use is dynamic. This should not be used in app development in general, but it is used when performing “dark arts” such as dynamic substitution methods or when deciding to implement such “dark arts” at runtime.

The good news about Swift and Objective-C mixing is that Apple is working hard to improve the SDK as Swift grows. The addition of nonnull and Nullable in Objective-C, as well as generic arrays and dictionaries, was an effort to make the SDK more suitable for Swift.

46. Optional protocols and protocol extensions

The protocol in Objective-C has the @optional keyword. Methods that are modified by this keyword do not have to be implemented. A series of methods can be defined through a protocol, and several of them can then be selectively implemented by the class that implements the protocol. Protocol methods are optional in many cases in Cocoa API, as opposed to the fact that all protocol methods in Swift must be implemented.

Methods that the protocol would not work without implementation are generally required, while methods that are relatively optional such as notification of events or configuration of non-critical properties are generally optional. The best examples would be UITableViewDataSource and UITableViewDelegate. There are two necessary methods in the former:

-tableView:numberOfRowsInSection:
-tableView:cellForRowAtIndexPath:
Copy the code

Methods used to calculate and prepare the height of the tableView and provide the style of each cell, respectively. Other methods, such as returning the number of sections or asking if the cell can be edited, have default behavior and are optional. All methods in the latter (UITableViewDelegate) are detailed configuration and event postback, so all are optional.

There are no options in the native Swift Protocol, and all defined methods are mandatory. If you want to define optional protocol methods as objective-C does, you define both the protocol itself and the optional methods as Objective-C, that is, at @objc before the protocol definition and before the protocol methods. And unlike @optional in Objective-C, we use the optional keyword without the @ sign to define optional methods:

 @objc protocol OptionalProtocol {
    @objc optional func optionalMethod(a)
}
Copy the code

In addition, for all declarations, their prefix qualifiers are completely separate. That is, you can’t specify that the next several methods are optional as you would in Objective-C with an @optional. You have to prefix each of the optional methods, which are mandatory by default for methods that don’t have prefixes:

@objc protocol OptionalProtocol {
    @objc optional func optionalMethod(a) / / is optional
    func necessaryMethod(a) / / must
    @objc optional func anotherOptionalMethod(a) / / is optional
}
Copy the code

An inevitable limitation is that protocol modified with @objc can only be implemented by class. That is, struct and enum types cannot be made to implement protocols with optional methods or properties. In addition, the method in the class that implements it must also be annotated as @objc, or the entire class simply inherits from NSObject.

In Swift 2.0, there is an alternative, which is to use Protocol Extension. You can declare a protocol and then use extension to give a partial default implementation of the method. These methods are then implemented as optional in real classes. Using the above example, the protocol extension would look like this:

protocol OptionalProtocol {
func optionalMethod(a) NecessaryMethod () func necessaryMethod() func anotherOptionalMethod() func necessaryMethod() func anotherOptionalMethod(
}
extension OptionalProtocol {
    func optionalMethod(a) {
        print("Implemented in extension")}func anotherOptionalMethod(a) {
        print("Implemented in extension")}}class MyClass: OptionalProtocol {
    func necessaryMethod(a) {
        print("Implemented in Class3")}func optionalMethod(a) {
        print("Implemented in Class3")}}let obj = MyClass()
obj.necessaryMethod() // Implemented in Class3
obj.optionalMethod()  // Implemented in Class3
obj.anotherOptionalMethod() // Implemented in extension
Copy the code

47. Memory management, weak and unowned

Swift automatically manages memory and does not need to worry about allocating or applying memory. When an object is created through initialization, Swift manages and allocates memory for us. The principle of freeing follows the automatic reference counting (ARC) rule: when an object has no references, its memory is automatically reclaimed. This mechanism greatly simplifies our coding, just making sure references are null when appropriate (out of scope, manually set to nil, etc.) to ensure that memory usage is not a problem.

However, one limitation that all automatic reference counting mechanisms have that is theoretically impossible to circumvent is the retain cycle situation.

 class A: NSObject {
    let b: B
    override init(a) {
        b = B(a)super.init()
        b.a = self 
    }
    deinit {
        print("A deinit")}}class B: NSObject {
    var a: A? = nil
    deinit {
        print("B deinit")}}Copy the code

To prevent this, you must give the compiler a hint that you don’t want them to hold each other. It is customary to expect the “passive” party not to hold the “active” party. Here, the holding of the instance of A in b.a is set by A’s method, and we directly use the instance of A later, so b is considered to be the passive party. You can change the above class B declaration to:

 class B: NSObject {
    weak var a: A? = nil
    deinit {
        print("B deinit")}}Copy the code

The weak prefix for var a tells the compiler that we don’t want to hold a. In this case, when obj points to nil, there’s no holding of this instance of A in the entire environment, so that instance can be freed.

In addition to weak, another Swift identifier that screams a similar “don’t quote me” identifier to the compiler is unowned. What’s the difference? “unowned” is more like “unsafe_unretained” and “weak” across objective-C. In plain English, the unowned setting remains an “invalid” reference to the freed object even if the original reference has been freed. It cannot be Optional and will not be referred to nil. If you try to call the referenced method or access a member attribute, the program will crash. Weak is friendlier, and members marked as weak will automatically become nil after the reference is released (so the variable marked as @weak must be Optional). As for the choice of using the two, Apple advises us to use unowned if we can be sure that the user will not be released during the visit. If there is a possibility that the user will be released, we choose weak.

The syntax for annotating at the location of a closure parameter is to place the content to be annotated before the original parameter, enclosed in brackets. If there are multiple elements that need to be marked, separate them with commas within the same brackets. For example:

/ / before
{ (number: Int) - >Bool in
    / /...
    return true 
}
/ / after annotation{[unowned self.weak someObject] (number: Int) - >Bool in
    / /...
    return true
}
Copy the code

48. @autoreleasepool

Swift uses automatic reference counting (ARC) for memory management, where you don’t need to manually call methods like retain, release, or autorelease to manage reference counting, But these methods are still called — it’s just that the compiler puts them in the right place at compile time. Retain and release are straightforward, adding or subtracting an object’s reference count. Autorelease, however, is a bit more special. It puts the objects receiving the message into a pre-established Auto Release pool and decreases the reference count of those objects by one when the drain message is received by the autorelease pool. They are then removed from the pool (a process figuratively known as “draining the pool”).

In the app, the entire main thread actually runs in an auto-release pool and drains at the end of each main Runloop. This is a necessary way to delay release, because sometimes you need to make sure that generated objects that are initialized inside a method are available to others when they are returned, rather than being released immediately.

In Objective-C, the syntax for setting up an automatic releasepool is simple: use @Autoreleasepool. Create a new Objective-C project and you can see the autoreleasepool for the entire project in main.m:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = UIApplicationMain(
            argc,
            argv,
            nil.NSStringFromClass([AppDelegate class]));
        returnretVal; }}Copy the code

Further, @AutoReleasepool is actually expanded to NSAutoreleasePool at compile time, with a call to the drain method.

In the Swift project, with @UIApplicationMain, we no longer need the main file and main function, so the original automatic release pool for the entire program is gone. Even if we use main.swift as the entry point to our program, we don’t need to add the auto-release pool ourselves.

However, there is one case where we still want automatic release, and that is when we are faced with generating a large number of AutoRelease objects in a method scope.

Autoreleasepool is also available in Swift — although the syntax is slightly different. Instead of a keyword in Objective-C, it is now a method that accepts closures:

func autoreleasepool(code: () - > ())

func loadBigData(a) {
    if let path = NSBundle.mainBundle()
        .pathForResource("big", ofType: "jpg") {
        for i in 1.10000 {
            autoreleasepool {
                 let data = NSData.dataWithContentsOfFile(
                    path, options: nil, error: nil)
                 NSThread.sleepForTimeInterval(0.5)}}}}Copy the code

Each cycle here generates an automatic release pool, which minimizes memory usage, but there are potential performance concerns with frequent releases. A compromise is to separate the loops into automatic release pools, such as one auto-release for every 10 loops, to reduce the performance penalty.

For this particular example, it is not necessary to include automatic release. Initialization methods are preferred in Swift rather than class methods like the one above, and since Swift 1.1, factory methods like the one in the above example have been removed from the API due to the addition of initializers that return nil. From now on we should all write:

let data = NSData(contentsOfFile: path)
Copy the code

With the initialization method, there is no need to deal with the problem of automatic release, and automatic memory management takes care of the memory-related matters every time the scope is exceeded.

49. Value and reference types

Swift has two types: value types, which are copied when passing and assigning values, and reference types, which use only one “point” of the reference object. Struct and enum types in Swift are value types, and class types are reference types. Interestingly, all of the built-in types in Swift are value types, including not only traditional meanings like Int and Bool, but also String, Array and Dictionary.

So what are the benefits of using value types? One obvious advantage over traditional reference types is that they reduce the number of memory allocations and deallocations on the heap. Swift’s value types, especially containers like arrays and dictionaries, are carefully designed for memory management. One of the features of value types is that they are copied on transfer and assignment. Each copy certainly incurs an overhead, but in Swift this cost is kept to a minimum and copying of value types does not occur when copying is not necessary. That is, simple assignment, parameter passing, and so on are common operations, although they may be set and passed back and forth with different names, but in memory they are the same piece of content. For example, code like this:

func test(_ arr: [Int]) {
    for i in arr {
        print(i) 
    }
}
var a = [1.2.3]
var b = a
let c = b
test(a)
Copy the code

In fact, the allocation only occurs when the first sentence a initializes the assignment, and the subsequent b, C, and even the ARR passed to the test method are the same physical memory as the original A. And this a is only in stack space, so this process for arrays, only pointer movement, and no heap memory allocation and free problem, which is very efficient.

Value types are copied when the contents of the value type change, such as when a number is added to b:

var a = [1.2.3]
var b = a
b.append(5)
// The memory addresses of a and B are not the same
Copy the code

When a value type is copied, all the value types stored in it are copied together. For a reference type, only one reference is copied. This is reasonable behavior because you don’t want the reference type to somehow refer to something other than the set:

class MyObject {
    var num = 0
}
var myObject = MyObject(a)var a = [myObject]
var b = a
b.append(myObject)
myObject.num = 100
print(b[0].num)   / / 100
print(b[1].num)   / / 100
// myObject changes affect both b[0] and b[1]
Copy the code

Although the primary consideration in designing arrays and dictionaries as value types is for thread-safety, this has another advantage when the number of elements or items stored is small, and that is, it is very efficient, because the “once assignment doesn’t change much” usage scenario is most common in Cocoa frameworks. This effectively reduces memory allocation and reclamation. But in a few cases, it’s obviously possible to store a lot of stuff in an array or dictionary, and to add or remove things from it. At this point, the container types of value types built into Swift need to be copied every time they operate, and the overhead of storing a large number of references during replication becomes significant, even if only the reference types are stored. Fortunately, there are Cocoa reference container classes for this, NSMutableArray and NSMutableDictionary.

Therefore, when using arrays and dictionaries, the best practice should be to decide whether to use value or reference containers based on the specific data size and operation characteristics: When you need to process a large amount of data and frequently manipulate (add or subtract) its elements, It is better to choose NSMutableArray and NSMutableDictionary, and for cases where the items in the container are small and the container itself is large, use the Swift language’s built-in Array and Dictionary.

50. String 还是 NSString

Nothing particularly noteworthy, but use the native String type whenever possible.

First of all, although String and NSString have nice conversions, all Cocoa apis now accept and return strings. There is no need or need to add trouble to converting strings returned from the framework. Since Cocoa encourages the use of strings, and provides sufficient methods for manipulating strings, why not just use them?

Second, because String is a struct in Swift, it fits the “immutable” nature of strings better than NSObject’s NSString class. In conjunction with constant assignment (LET), this invariance is important in multithreaded programming, in principle freeing programmers from worries about memory access and operation order. In addition, without touching the nsString-specific operations and dynamic nature, using the String method also provides performance gains.

Finally, because String implements a protocol like Collection, there are some Swift syntactic features that only String can use that NSString does not. One example is for… Enumeration of in. If you convert to NSString, you can’t use for… In and enumerates.

But there are exceptions. There are some NSString methods that are not implemented in Strings, and one useful one is contains, which is new in iOS 8. If you want to use this API to simply determine that a string contains a substring, you must first convert it to NSString.

In Swift 3 Apple has added the contains method to String. Now we can write levels. Contains (“BC”) directly.

The only tricky aspect of using String is its combination with Range. In NSString, NSRange is usually used to represent the result or as input when matching strings. When using the String API, NSRange is also mapped to its special version of String in Swift: Range< string.index >. This can be very annoying at times:

let levels = "ABCDE"
let nsRange = NSMakeRange(1.4)
// Error compiling
// Cannot convert value of type `NSRanve` to expected argument type 'Range<Index>' levels.replacingCharacters(in: nsRange, with: "AAAA")

let indexPositionOne = levels.index(levels.startIndex, offsetBy: 1)
let swiftRange = indexPositionOne ..< levels.index(levels.startIndex, offsetBy: 5) 
levels.replacingCharacters(in: swiftRange, with: "AAAA")
/ / output:
// AAAAA
Copy the code

In general, you might prefer to work with int-based NSranges rather than the cumbersome Range< string.index >. In this case, converting String to NSString might be a good choice:

let nsRange = NSMakeRange(1.4)
(levels as NSString).replacingCharacters(in: nsRange, with: "AAAA")
Copy the code