文森說技術

iOS, Web Development Notes
- ,

用用看 ListFormatter, "烤肉、月餅和柚子"

本文使用環境或工具版本

Xcode12.0
Swift5.3

這是在 WWDC 2019 上 723. Advances in Foundation 中提到的一個可以說是語法糖衣的東西。實務上除了 SNS 或是公司骨幹系統一些人性化的表示,是有點想不太到他可以用自哪邊 XDD

不過會想來玩玩是因為在 Twitter 上滑到最近這則 推文 和他提到的 Formatting Notes and Gotchas 於是就來認真玩玩看。

官方的文件

documentation

算是 Apple 的正常發揮,沒有進一步的詳細訊息 XDDD
但是這的類別也不會太複雜,看看 WWDC 的影片 和稍微試一下就可以知道怎麼用了。

還想要知道更細節可以看標頭檔,網頁上沒寫的這邊其實都寫的很清楚。由於篇幅滿大的所以我把它收闔起來,有興趣的人請再自己打開來看:

打開看 ListFormatter 的定義
ListFormatter.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import CoreFoundation

/* NSListFormatter.h
Copyright (c) 2018-2019, Apple Inc. All rights reserved.
*/

/* NSListFormatter provides locale-correct formatting of a list of items using the appropriate separator and conjunction. Note that the list formatter is unaware of the context where the joined string will be used, e.g., in the beginning of the sentence or used as a standalone string in the UI, so it will not provide any sort of capitalization customization on the given items, but merely join them as-is. The string joined this way may not be grammatically correct when placed in a sentence, and it should only be used in a standalone manner.
*/

@available(iOS 13.0, *)
open class ListFormatter : Formatter {


// !__OBJC2__

/* Specifies the locale to format the items. Defaults to autoupdatingCurrentLocale. Also resets to autoupdatingCurrentLocale on assignment of nil.
*/
open var locale: Locale!


/* Specifies how each object should be formatted. If not set, the object is formatted using its instance method in the following order: -descriptionWithLocale:, -localizedDescription, and -description.
*/
@NSCopying open var itemFormatter: Formatter?


/* Convenience method to return a string constructed from an array of strings using the list format specific to the current locale. It is recommended to join only disjointed strings that are ready to display in a bullet-point list. Sentences, phrases with punctuations, and appositions may not work well when joined together.
*/
open class func localizedString(byJoining strings: [String]) -> String


/* Convenience method for -stringForObjectValue:. Returns a string constructed from an array in the locale-aware format. Each item is formatted using the itemFormatter. If the itemFormatter does not apply to a particular item, the method will fall back to the item's -descriptionWithLocale: or -localizedDescription if implemented, or -description if not.

Returns nil if `items` is nil or if the list formatter cannot generate a string representation for all items in the array.
*/
open func string(from items: [Any]) -> String?


/* Inherited from NSFormatter. `obj` must be an instance of NSArray. Returns nil if `obj` is nil, not an instance of NSArray, or if the list formatter cannot generate a string representation for all objects in the array.
*/
open func string(for obj: Any?) -> String?
}

工具

本篇都是在 Xcode 12.0 的 Playground 裡面執行。

ListFormatter 可以做什麼

把一個資料的陣列,搭配 localization ,轉換成我們人看的懂得文字。

例如有以下的陣列,因為最近中秋節剛過就拿這個當例子吧:

1
2
["烤肉", "月餅"] // 兩筆
["烤肉", "月餅", "柚子"] // 三筆

經過轉換之後,就會變成這樣。根據不同的語言,還會有不同的表示:

語言 轉換後的字串 - 兩筆 轉換後的字串 - 三筆
zh_TW 烤肉和月餅 烤肉、月餅和柚子
ja_JP 烤肉、月餅 烤肉、月餅、柚子
en_US 烤肉 and 月餅 烤肉, 月餅, and 柚子
en_UK 烤肉 and 月餅 烤肉, 月餅 and 柚子

看了轉換後的字串可以發現,他不只幫我們挑語言適合的連接詞和逗號(頓號),還幫我們判斷兩個,或三個及以上該怎麼呈現都幫我們打理好了。在有這個糖衣類別出來之前我們都要自己寫一些 helper methods 來處理。

另外看了 Formatting Notes and Gotchas 才知道原來美國會比英國多一個逗號。多找了一下資料才發現這個叫 牛津逗號 的東西可以避免一些文書表達上的誤會 (參考 1, 參考 2)。

還有很驚訝日文沒有用

Get Hands Dirty

基本用法

先宣告一個陣列

1
let moonFestival = ["烤肉", "月餅", "柚子"]

使用 .localizedString(byJoining:)

最簡單的用法如下

1
ListFormatter.localizedString(byJoining: moonFestival)

就會得到以下的結果

1
烤肉, 月餅, and 柚子

.localizedString(byJoining:) 的時候,他會自動取得目前的 locale 資訊,組出相對應的字串並回傳

所以如果動態改變語言設定,他也會跟著改變:

1
2
3
UserDefaults.standard.set(["zh_TW"], forKey: "AppleLanguages")
ListFormatter.localizedString(byJoining: moonFestival)
// result: "烤肉、月餅和柚子"

初始化並自定 Locale

除了用系統預設的 locale 之外,初始化 ListFormatter 之後,也可以自行設定語言:

1
2
3
4
let listFormatter = ListFormatter()
listFormatter.locale = Locale(identifier: "zh_TW")
listFormatter.string(from: moonFestival)
// result: "烤肉、月餅和柚子"

非字串輸入與 itemFormatter

從產出格式化字串的方法定義可以知道,輸入的資料不是只有字串陣列,而是可以任何型別的 Any 陣列

1
open func string(from items: [Any]) -> String?

而我們也可以透過指定 itemFormatter 這個屬性來設定每一個元素要該怎麼格式化。

1
@NSCopying open var itemFormatter: Formatter?

如果沒有指定的話,還會幫我們 fallback 找到適合的字串組合好並回傳。

Date 陣列

以這個陣列為例

1
2
3
4
5
let dates = [
Date(timeIntervalSince1970: 100_000_000),
Date(timeIntervalSince1970: 200_000_000),
Date(timeIntervalSince1970: 300_000_000),
]

厲害的地方是不用改變 itemFormatter 就可以有東西輸出:

1
2
3
4
5
let listFormatter = ListFormatter()

listFormatter.locale = Locale(identifier: "zh_TW")
listFormatter.string(from: dates)
// result: "1973年3月3日 星期六 日本標準時間 下午6:46:40、1976年5月4日 星期二 日本標準時間 上午4:33:20和1979年7月5日 星期四 日本標準時間 下午2:20:00"

也可以設定自己的 date formatter :

1
2
3
4
5
6
7
8
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short

let listFormatter = ListFormatter()
listFormatter.locale = Locale(identifier: "zh_TW")
listFormatter.itemFormatter = dateFormatter
listFormatter.string(from: dates)
// result: "1973年3月3日、1976年5月4日和1979年7月5日"

自定義 model 陣列

Model 的定義和陣列如下

1
2
3
struct Food {
let name: String
}
1
2
3
4
5
let items = [
Food(name: "烤肉"),
Food(name: "月餅"),
Food(name: "柚子"),
]

格式化時直接丟進去會變這樣:

1
2
3
4
let listFormatter = ListFormatter()
listFormatter.locale = Locale(identifier: "zh_TW")
listFormatter.string(from: items)
// result: "__lldb_expr_137.Food(name: "烤肉")、__lldb_expr_137.Food(name: "月餅")和__lldb_expr_137.Food(name: "柚子")"

會印出赤裸裸的 struct 相關資訊的字串

這時候有至少兩個方法來避開

傳入前整形

這應該是最輕鬆的方法,透過 map 和 KeyPath 來整形輸入的字串。程式碼也很好閱讀和理解。

1
2
lf.string(from: items.map(\.name))
// result: "烤肉、月餅和柚子"
自定義 Formatter

我們可以自定義一個 Formatter ,用點是可以把格式化的邏輯再封裝起來。

1
2
3
4
5
final class FoodFormatter: Formatter {
override func string(for obj: Any?) -> String? {
return (obj as? Food)?.name
}
}

實作上雖然非 Food 型別的物件會回傳 nil ,但是 ListFormatter 內部在接到空值之後會自己在去找適合的字串來組合。

接著來套用看看:

1
2
3
listFormatter.itemFormatter = FoodFormatter()
listFormatter.string(from: items)
// result: "烤肉、月餅和柚子"

從結果看,就可以看到輸出和自己預期的一樣了。

以上就是 ListFormatter 的一些基本和可能會有的變化用法,
這篇就到這邊,不妨也自己動手做做看吧!

如果覺得這篇對你有幫助,歡迎幫忙分享給其他人 😀