A Flattening Map (33)
Usually when you want to query an array, the conversion function returns another array and not just one element. For example, we have a method, links
, that can read the Markdown file and return an array containing the URL of all the paths in the file. That method looks like this:
1 2 | func extractLinks(markdownFile: String) -> [URL] |
If we have a bunch of Markdown files and want to extract the path from all the files into a single array, we can try to write some tests like markdownFiles.map(extractLinks)
. But it returns an array of an array containing URLs: one file for each array. Now you just practice map, get back an array of arrays and then flatten them to get a single array:
1 2 3 4 | let markdownFiles: [String] = // ... let nestedLinks = markdownFiles.map(extractLinks) let links = nestedLinks.joined() |
The flatMap
function is a combination of 2 operators in 1 step. So markdownFiles.flatMap(extractLinks)
returns all URLs in the array of each Markdown file as a single array.
Basically flatMap is similar to map , except that its input method is transformed into an array. To make it complete, it uses append (contentsOf 🙂 instead of append ( in flattening the resulting array:
1 2 3 4 5 6 7 8 | extension Array { func flatMap<T>(_ transform: (Element) -> [T]) -> [T] { var result: [T] = [] for x in self{ result.append(contentsOf: transform(x)) } return result } } |
Another better way to use flatMap is to combine elements from different arrays. To facilitate the combination of arrays, flatMap on one array and map on the other:
1 2 3 4 5 6 7 8 9 10 11 12 13 | let suits = ["♠", "♥", "♣", "♦"] let ranks = ["J","Q","K","A"] let result = suits.flatMap { suit in ranks.map { rank in (suit, rank) } } /* [("♠", "J"), ("♠", "Q"), ("♠", "K"), ("♠", "A"), ("♥", "J"), ("♥", "Q"), ("♥", "K"), ("♥", "A"), ("♣", "J"), ("♣", "Q"), ("♣", "K"), ("♣", "A"), ("♦", "J"), ("♦", "Q"), ("♦", "K"), ("♦", "A")] */ |
Iteration using forEach
The last operator we want to discuss is forEach. It works almost like a loop: The input method is executed each time for an object in the sequence. And unlike the map, forEach returns nothing. Let’s start with replacing loops with forEach:
1 2 3 4 5 6 7 8 | for element in [1,2,3] { print(element) } [1,2,3].forEach { element in print(element) } |
There are no major changes, but it can be helpful if you make a call to each element in the string. Using the function named forEach instead of closure exoresion can be shorter and easier to understand. For example, if you are in a view controller and want to add a subview array to the main view, just write theViews.forEach (view.addSubview).
However, there are some minor differences between loops and forEach. Typically, if a loop has a return statement in it, rewriting it with forEach may not require significant code changes. Consider the example below, which is written using a loop with the where condition
1 2 3 4 5 6 7 8 9 | extension Array where Element: Equatable { func index(of element: Element) -> Int? { for idx in self.indices where self[idx] == element { return idx } return nil } } |
We cannot copy the where statement directly in the forEach structure, so we can rewrite (not exactly the same) by using the filter:
1 2 3 4 5 6 7 8 9 | extension Array where Element: Equatable { func index_foreach(of element: Element) -> Int? { self.indices.filter { idx in self[idx] == element }.forEach { idx in return idx } return nil } } |
The return value in the forEach closure does not return outside the function, it only returns inside its closure. In a practical case, we can detect a bug because the compiler will generate a warning about the unused argument being returned, but you should not rely on it to suggest a solution.
Also, consider the simple example below:
1 2 3 4 5 | (1..<10).forEach { number in print(number) if number > 2 {return } } |
One thing that is not really clear is the printing of all numbers in the input range. The return statement does not break the loop, instead it goes back to the closure. In some situations, like the addSubview example above, forEach might be better than looping. However, since it is not broken by return, we only recommend it for its main function, in addition to using the regular loop instead.
Array types
Slices
Normally, to access a single element in an array with position (eg fibs [0]) we can also access multiple positions of an element via subscript. For example, to get all but the first object of the array, we use:
1 2 3 4 | let slice = fibs[1...] slice // [1, 1, 2, 3, 5] type(of: slice) // ArraySlice<Int> |
With slice arrays starting at the second element, the type of result returned is ArraySlice, not Array, ArraySlice is a view of the array. It is supported by the original array, however it provides many based on slices. This makes sure the array does not need replication. The ArraySlice type has the same identifier functions as the Array, so you can use a slice as an array if it was previously an array. If you want to convert a slice into an array, you need to structure a new array from the slice:
1 2 3 | let newArray = Array(slice) type(of: newArray) // Array<Int> |
Bridging
Swift arrays can be bridged with OBjective-C. It can also be used with C, but we can convert in subsequent chapters. Because NSArray can only hold symbolic teams, the compiler and runtime automatically wrap incompatible values (such as enum) in an unknown object. Some value types (such as Int, Bool, and String, but including Dictionary and Set) are bridged by default with its object type in Objective – C
1 2 | Cơ chế cầu nối giữa tất cả loại của Swift với Objective-C không chỉ giúp cho việc sử dụng với mảng dễ dàng hơn. Nó cũng cho phép điều đó với những kiểu nhóm khác, như dictionary và set, nó cũng mở ra những chức năng trong tưuong lai tiềm năng giữa Swift và Objective-C. Ví dụ, phiên bản tương lai của Swift cho phép kiểu giá trị Swift cũng phù hợp với @objc protocol |