├── Example.png
├── .github
└── FUNDING.yml
├── Example
├── Demo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── MacDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── MacDemo.entitlements
│ ├── AppDelegate.swift
│ ├── ViewController.swift
│ └── Base.lproj
│ │ └── Main.storyboard
└── Demo.xcodeproj
│ ├── project.xcworkspace
│ └── contents.xcworkspacedata
│ └── project.pbxproj
├── .gitignore
├── Package.swift
├── License
├── Sources
└── ListFormatter
│ ├── ListType.swift
│ ├── MarkerGenerator.swift
│ └── NSAttributedString+ListFormatter.swift
├── Tests
└── FormattedListKitTests
│ └── MarkerGeneratorTests.swift
└── README.md
/Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chiahsien/FormattedListKit/HEAD/Example.png
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | buy_me_a_coffee: chiahsien
4 |
--------------------------------------------------------------------------------
/Example/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/MacDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/Example/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/MacDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/MacDemo/MacDemo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/MacDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MacDemo
4 | //
5 | // Created by Nelson on 2025/3/25.
6 | //
7 |
8 | import Cocoa
9 |
10 | @main
11 | class AppDelegate: NSObject, NSApplicationDelegate {
12 |
13 |
14 |
15 |
16 | func applicationDidFinishLaunching(_ aNotification: Notification) {
17 | // Insert code here to initialize your application
18 | }
19 |
20 | func applicationWillTerminate(_ aNotification: Notification) {
21 | // Insert code here to tear down your application
22 | }
23 |
24 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
25 | return true
26 | }
27 |
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Example/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | }
30 | ],
31 | "info" : {
32 | "author" : "xcode",
33 | "version" : 1
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Example/Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 | UISceneStoryboardFile
19 | Main
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "FormattedListKit",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, making them visible to other packages.
14 | .library(
15 | name: "FormattedListKit",
16 | targets: ["FormattedListKit"]),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package, defining a module or a test suite.
20 | // Targets can depend on other targets in this package and products from dependencies.
21 | .target(
22 | name: "FormattedListKit",
23 | path: "Sources"),
24 | .testTarget(
25 | name: "FormattedListKitTests",
26 | dependencies: ["FormattedListKit"]
27 | ),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/License:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 FormattedListKit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Example/MacDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Example/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Example/Demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/Sources/ListFormatter/ListType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListType.swift
3 | // FormattedListKit
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Defines styles for ordered lists.
11 | public enum OrderedListStyle: Hashable {
12 | case decimal
13 | case lowerRoman
14 | case upperRoman
15 | case lowerAlpha
16 | case upperAlpha
17 | }
18 |
19 | /// Defines styles for unordered lists.
20 | public enum UnorderedListStyle: Hashable {
21 | case disc
22 | case circle
23 | case square
24 | case custom(String)
25 | }
26 |
27 | /// Represents the type of list, either ordered or unordered, with its associated style.
28 | public enum ListType: Hashable {
29 | case ordered(style: OrderedListStyle)
30 | case unordered(style: UnorderedListStyle)
31 | }
32 |
33 | // MARK: - CustomStringConvertible Conformance
34 | extension ListType: CustomStringConvertible {
35 | public var description: String {
36 | switch self {
37 | case .ordered(let style):
38 | switch style {
39 | case .decimal: return "Decimal"
40 | case .lowerRoman: return "Lower Roman"
41 | case .upperRoman: return "Upper Roman"
42 | case .lowerAlpha: return "Lower Alpha"
43 | case .upperAlpha: return "Upper Alpha"
44 | }
45 | case .unordered(let style):
46 | switch style {
47 | case .disc: return "Disc"
48 | case .circle: return "Circle"
49 | case .square: return "Square"
50 | case .custom(let symbol): return "Custom (\(symbol))"
51 | }
52 | }
53 | }
54 | }
55 |
56 | /// Defines alignment options for list markers.
57 | public enum MarkerAlignment: Hashable, CustomStringConvertible {
58 | case left
59 | case right
60 |
61 | public var description: String {
62 | switch self {
63 | case .left: return "Left"
64 | case .right: return "Right"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/FormattedListKitTests/MarkerGeneratorTests.swift:
--------------------------------------------------------------------------------
1 | import Testing
2 | import UIKit
3 | @testable import FormattedListKit
4 |
5 | struct MarkerGeneratorTests {
6 | // MARK: - Roman Numeral Tests
7 |
8 | @Test("Roman numeral conversion", arguments: [
9 | (1, false, "i"), (4, true, "IV"), (10, false, "x"), (1999, true, "MCMXCIX"),
10 | (5, false, "v"), (9, true, "IX"), (50, false, "l"), (100, true, "C")
11 | ])
12 | func testRomanNumeral(number: Int, isUpper: Bool, expected: String) {
13 | let result = MarkerGenerator.romanNumeral(for: number, isUpper: isUpper)
14 | #expect(result == expected)
15 | }
16 |
17 | // MARK: - Alpha Numeral Tests
18 |
19 | @Test("Alpha numeral conversion", arguments: [
20 | (1, false, "a"), (2, true, "B"), (27, false, "aa"), (52, true, "AZ"),
21 | (3, false, "c"), (26, true, "Z"), (28, false, "ab"), (702, true, "ZZ")
22 | ])
23 | func testAlphaNumeral(number: Int, isUpper: Bool, expected: String) {
24 | let result = MarkerGenerator.alphaNumeral(for: number, isUpper: isUpper)
25 | #expect(result == expected)
26 | }
27 |
28 | // MARK: - Marker Tests
29 |
30 | @Test("Ordered list marker generation", arguments: [
31 | (0, OrderedListStyle.decimal, "1."),
32 | (1, OrderedListStyle.lowerRoman, "ii."),
33 | (2, OrderedListStyle.upperRoman, "III."),
34 | (3, OrderedListStyle.lowerAlpha, "d."),
35 | (25, OrderedListStyle.upperAlpha, "Z.")
36 | ])
37 | func testOrderedMarker(index: Int, style: OrderedListStyle, expected: String) {
38 | let result = MarkerGenerator.marker(for: style, number: index + 1)
39 | #expect(result == expected)
40 | }
41 |
42 | @Test("Unordered list marker generation", arguments: [
43 | (0, UnorderedListStyle.disc, "•"),
44 | (1, UnorderedListStyle.circle, "◦"),
45 | (2, UnorderedListStyle.square, "▪"),
46 | (3, UnorderedListStyle.custom("★"), "★")
47 | ])
48 | func testUnorderedMarker(index: Int, style: UnorderedListStyle, expected: String) {
49 | let result = MarkerGenerator.marker(for: style)
50 | #expect(result == expected)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Example/Demo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Demo
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Sources/ListFormatter/MarkerGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkerGenerator.swift
3 | // FormattedListKit
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | import Foundation
9 |
10 | internal struct MarkerGenerator {
11 | // Generates a string for a list marker based on the ordered list type and index
12 | static func marker(for style: OrderedListStyle, number: Int) -> String {
13 | switch style {
14 | case .decimal:
15 | return "\(number)."
16 | case .lowerRoman:
17 | return romanNumeral(for: number, isUpper: false) + "."
18 | case .upperRoman:
19 | return romanNumeral(for: number, isUpper: true) + "."
20 | case .lowerAlpha:
21 | return alphaNumeral(for: number, isUpper: false) + "."
22 | case .upperAlpha:
23 | return alphaNumeral(for: number, isUpper: true) + "."
24 | }
25 | }
26 |
27 | // Generates a string for a list marker based on the unordered list type
28 | static func marker(for style: UnorderedListStyle) -> String {
29 | switch style {
30 | case .disc:
31 | return "•"
32 | case .circle:
33 | return "◦"
34 | case .square:
35 | return "▪"
36 | case .custom(let symbol):
37 | return symbol.trimmingCharacters(in: .whitespacesAndNewlines)
38 | }
39 | }
40 |
41 | // Converts an integer to a Roman numeral string
42 | static func romanNumeral(for number: Int, isUpper: Bool) -> String {
43 | let romanValues = [
44 | (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
45 | (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
46 | (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")
47 | ]
48 | var result = ""
49 | var num = number
50 | for (value, symbol) in romanValues {
51 | while num >= value {
52 | result += symbol
53 | num -= value
54 | }
55 | }
56 | return isUpper ? result : result.lowercased()
57 | }
58 |
59 | // Converts an integer to an alphabetic string (e.g., "a", "b", "aa")
60 | static func alphaNumeral(for number: Int, isUpper: Bool) -> String {
61 | var num = number
62 | var result = ""
63 | while num > 0 {
64 | num -= 1
65 | let char = String(UnicodeScalar((num % 26) + 97)!)
66 | result = char + result
67 | num /= 26
68 | }
69 | return isUpper ? result.uppercased() : result
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Example/Demo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Demo
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | import UIKit
9 | import FormattedListKit
10 | import Combine
11 |
12 | final class ViewController: UIViewController {
13 | @IBOutlet weak var textView: UITextView!
14 |
15 | @Published private var alignment: MarkerAlignment = .left
16 | @Published private var type: ListType = .ordered(style: .decimal)
17 |
18 | private var cancelables: Set = []
19 | private let items: [String] = [
20 | // English
21 | "Apple", "The quick brown fox", "Hello world", "Swift programming", "Short", "A very long sentence to test scrolling functionality in the list A very long sentence to test scrolling functionality in the list",
22 | // Chinese
23 | "蘋果", "快速的棕色狐狸", "你好世界", "程式設計", "短", "這是一個很長的句子用來測試列表的滾動功能 這是一個很長的句子用來測試列表的滾動功能",
24 | // Japanese
25 | "リンゴ", "速い茶色のキツネ", "こんにちは世界", "プログラミング", "短い", "スクロール機能をテストするための非常に長い文 スクロール機能をテストするための非常に長い文",
26 | // Korean
27 | "사과", "빠른 갈색 여우", "안녕하세요 세계", "프로그래밍", "짧은", "리스트の 스크롤 기능을 테스트하기 위한 매우 긴 문장 리스트の 스크롤 기능을 테스트하기 위한 매우 긴 문장",
28 | // Thai
29 | "แอปเปิ้ล", "จิ้งจอกสีน้ำตาลเร็ว", "สวัสดีชาวโลก", "การเขียนโปรแกรม", "สั้น", "ประโยคที่ยาวมากเพื่อทดสอบการเลื่อนของรายการ ประโยคที่ยาวมากเพื่อทดสอบการเลื่อนของรายการ"
30 | ]
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | Publishers.CombineLatest($alignment, $type)
36 | .sink { [weak self] alignment, type in
37 | guard let self else { return }
38 | updateList(withMarkAlignment: alignment, markerType: type)
39 | }
40 | .store(in: &cancelables)
41 | }
42 |
43 | @IBAction func markerAlignmentDidChange(_ sender: UISegmentedControl) {
44 | alignment = sender.selectedSegmentIndex == 0 ? .left : .right
45 | }
46 |
47 | @IBAction func markerTypeDidChange(_ sender: UISegmentedControl) {
48 | switch sender.selectedSegmentIndex {
49 | case 0: type = .ordered(style: .decimal)
50 | case 1: type = .ordered(style: .upperAlpha)
51 | case 2: type = .ordered(style: .lowerRoman)
52 | case 3: type = .unordered(style: .square)
53 | case 4: type = .unordered(style: .custom("★"))
54 | default: type = .ordered(style: .decimal)
55 | }
56 | }
57 |
58 | private func updateList(withMarkAlignment alignment: MarkerAlignment, markerType: ListType) {
59 | textView.attributedText = NSAttributedString.createList(for: items, type: markerType, font: .systemFont(ofSize: 17), markerAlignment: alignment)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Example/MacDemo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // MacDemo
4 | //
5 | // Created by Nelson on 2025/3/25.
6 | //
7 |
8 | import Cocoa
9 | import FormattedListKit
10 | import Combine
11 |
12 | class ViewController: NSViewController {
13 | @IBOutlet var textView: NSTextView!
14 |
15 | @Published private var alignment: MarkerAlignment = .left
16 | @Published private var type: ListType = .ordered(style: .decimal)
17 |
18 | private var cancelables: Set = []
19 | private let items: [String] = [
20 | // English
21 | "Apple", "The quick brown fox", "Hello world", "Swift programming", "Short", "A very long sentence to test scrolling functionality in the list A very long sentence to test scrolling functionality in the list",
22 | // Chinese
23 | "蘋果", "快速的棕色狐狸", "你好世界", "程式設計", "短", "這是一個很長的句子用來測試列表的滾動功能 這是一個很長的句子用來測試列表的滾動功能",
24 | // Japanese
25 | "リンゴ", "速い茶色のキツネ", "こんにちは世界", "プログラミング", "短い", "スクロール機能をテストするための非常に長い文 スクロール機能をテストするための非常に長い文",
26 | // Korean
27 | "사과", "빠른 갈색 여우", "안녕하세요 세계", "프로그래밍", "짧은", "리스트の 스크롤 기능을 테스트하기 위한 매우 긴 문장 리스트の 스크롤 기능을 테스트하기 위한 매우 긴 문장",
28 | // Thai
29 | "แอปเปิ้ล", "จิ้งจอกสีน้ำตาลเร็ว", "สวัสดีชาวโลก", "การเขียนโปรแกรม", "สั้น", "ประโยคที่ยาวมากเพื่อทดสอบการเลื่อนของรายการ ประโยคที่ยาวมากเพื่อทดสอบการเลื่อนของรายการ"
30 | ]
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | Publishers.CombineLatest($alignment, $type)
36 | .sink { [weak self] alignment, type in
37 | guard let self else { return }
38 | updateList(withMarkAlignment: alignment, markerType: type)
39 | }
40 | .store(in: &cancelables)
41 | }
42 |
43 | @IBAction func markerAlignDidChange(_ sender: NSSegmentedControl) {
44 | alignment = sender.selectedSegment == 0 ? .left : .right
45 | }
46 |
47 | @IBAction func markerTypeDidChange(_ sender: NSSegmentedControl) {
48 | switch sender.selectedSegment {
49 | case 0: type = .ordered(style: .decimal)
50 | case 1: type = .ordered(style: .upperAlpha)
51 | case 2: type = .ordered(style: .lowerRoman)
52 | case 3: type = .unordered(style: .square)
53 | case 4: type = .unordered(style: .custom("★"))
54 | default: type = .ordered(style: .decimal)
55 | }
56 | }
57 |
58 | private func updateList(withMarkAlignment alignment: MarkerAlignment, markerType: ListType) {
59 | textView.textStorage?.setAttributedString(
60 | NSAttributedString.createList(
61 | for: items,
62 | type: markerType,
63 | font: .systemFont(ofSize: 17),
64 | markerAlignment: alignment
65 | )
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FormattedListKit
2 |
3 | A Swift package for creating and formatting ordered and unordered lists as `NSAttributedStrings` with precise control over markers and alignment.
4 |
5 | ## Features
6 | - [x] Create ordered lists with various numbering styles (decimal, roman numerals, alphabetical)
7 | - [x] Create unordered lists with different bullet styles (disc, circle, square, custom)
8 | - [x] Control marker alignment (left or right aligned)
9 | - [x] Proper indentation and alignment for wrapped text
10 | - [x] Customizable font for list items
11 | - [x] Monospaced markers for consistent alignment
12 | - [x] Seamless integration with NSAttributedString
13 |
14 |
15 |
16 | ## Requirements
17 | - iOS 13.0+ / macOS 10.15+
18 | - Swift 5.5+
19 |
20 | ## Installation
21 |
22 | ### Swift Package Manager
23 |
24 | Add FormattedListKit to your project using Swift Package Manager:
25 |
26 | ```swift
27 | dependencies: [
28 | .package(url: "https://github.com/chiahsien/FormattedListKit.git", from: "1.0.0")
29 | ]
30 | ```
31 |
32 | ## Example
33 | Check out the **Demo** project in `Example` folder.
34 |
35 | 
36 |
37 | ## Usage
38 |
39 | ### Basic Usage
40 |
41 | ```swift
42 | import FormattedListKit
43 | import UIKit
44 |
45 | // Create a simple ordered list
46 | let items = ["First item", "Second item with longer text that will wrap", "Third item"]
47 | let attributedString = NSAttributedString.createList(
48 | for: items,
49 | type: .ordered(style: .decimal)
50 | )
51 |
52 | // Set it to a UILabel or UITextView
53 | myTextView.attributedText = attributedString
54 | ```
55 |
56 | ### Customizing List Styles
57 |
58 | #### Ordered Lists
59 |
60 | ```swift
61 | // Decimal numbers (1. 2. 3.)
62 | let decimalList = NSAttributedString.createList(
63 | for: items,
64 | type: .ordered(style: .decimal)
65 | )
66 |
67 | // Lowercase Roman numerals (i. ii. iii.)
68 | let romanList = NSAttributedString.createList(
69 | for: items,
70 | type: .ordered(style: .lowerRoman)
71 | )
72 |
73 | // Uppercase alphabetical (A. B. C.)
74 | let alphaList = NSAttributedString.createList(
75 | for: items,
76 | type: .ordered(style: .upperAlpha)
77 | )
78 | ```
79 |
80 | #### Unordered Lists
81 |
82 | ```swift
83 | // Bullet points (•)
84 | let bulletList = NSAttributedString.createList(
85 | for: items,
86 | type: .unordered(style: .disc)
87 | )
88 |
89 | // Circles (◦)
90 | let circleList = NSAttributedString.createList(
91 | for: items,
92 | type: .unordered(style: .circle)
93 | )
94 |
95 | // Custom marker (★)
96 | let customList = NSAttributedString.createList(
97 | for: items,
98 | type: .unordered(style: .custom("★"))
99 | )
100 | ```
101 |
102 | ### Customizing Appearance
103 |
104 | ```swift
105 | // Custom font and marker alignment
106 | let customList = NSAttributedString.createList(
107 | for: items,
108 | type: .ordered(style: .decimal),
109 | font: UIFont.systemFont(ofSize: 16, weight: .medium),
110 | markerAlignment: .left
111 | )
112 | ```
113 |
114 | ## License
115 |
116 | This library is released under the MIT license. See [LICENSE](License) for details.
117 |
--------------------------------------------------------------------------------
/Sources/ListFormatter/NSAttributedString+ListFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+FormattedListKit.swift
3 | // FormattedListKit
4 | //
5 | // Created by Nelson on 2025/3/24.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import UIKit
10 | public typealias PlatformFont = UIFont
11 | #elseif canImport(AppKit)
12 | import AppKit
13 | public typealias PlatformFont = NSFont
14 | #endif
15 |
16 | extension NSAttributedString {
17 | /// Creates an attributed string formatted as a list with markers.
18 | ///
19 | /// This method generates a list from an array of strings, applying the specified list type and font.
20 | /// The list markers (e.g., numbers or bullets) are aligned according to the specified marker alignment,
21 | /// and the text is left-aligned with proper indentation for wrapped lines.
22 | ///
23 | /// - Parameters:
24 | /// - items: An array of strings representing the list items.
25 | /// - type: The type of list, either ordered (e.g., numbered) or unordered (e.g., bulleted).
26 | /// - font: The font used for the list. Defaults to the system font with a size of 17 points.
27 | /// - markerAlignment: The alignment of the list markers. Defaults to `.right`.
28 | /// - spacing: The spacing between the marker and the text. Defaults to 8.
29 | /// - Returns: An `NSAttributedString` representing the formatted list.
30 | public static func createList(
31 | for items: [String],
32 | type: ListType,
33 | font: PlatformFont = .systemFont(ofSize: 17, weight: .regular),
34 | markerAlignment: MarkerAlignment = .right,
35 | spacing: CGFloat = 8.0
36 | ) -> NSAttributedString {
37 | // Calculate the maximum width of the markers
38 | let markerFont = PlatformFont.monospacedSystemFont(ofSize: font.pointSize, weight: .regular)
39 | let markerWidth = markerWidth(for: type, itemCount: items.count, font: markerFont)
40 |
41 | // Define spacing between marker and text
42 | let firstLocation: CGFloat
43 | let secondLocation: CGFloat = markerWidth + spacing
44 |
45 | // Configure paragraph style for list formatting
46 | let alignment: NSTextAlignment
47 | switch markerAlignment {
48 | case .left:
49 | alignment = .left
50 | firstLocation = 1 // Set to 0 will cause unexpected layout behavior.
51 | case .right:
52 | alignment = .right
53 | firstLocation = markerWidth + 1
54 | }
55 |
56 | // Configure paragraph style for list formatting
57 | let tabStops = [
58 | NSTextTab(textAlignment: alignment, location: firstLocation),
59 | NSTextTab(textAlignment: .left, location: secondLocation)
60 | ]
61 |
62 | let paragraphStyle = NSMutableParagraphStyle()
63 | paragraphStyle.tabStops = tabStops
64 | paragraphStyle.headIndent = secondLocation
65 | paragraphStyle.alignment = .left
66 | paragraphStyle.lineBreakMode = .byWordWrapping
67 |
68 | // Create attributed strings for each list item
69 | let markerAttributes: [NSAttributedString.Key: Any] = [
70 | .font: markerFont,
71 | .paragraphStyle: paragraphStyle
72 | ]
73 | let itemAttributes: [NSAttributedString.Key: Any] = [
74 | .font: font,
75 | .paragraphStyle: paragraphStyle
76 | ]
77 |
78 | let combinedAttr = NSMutableAttributedString()
79 | for (index, item) in items.enumerated() {
80 | let marker = marker(for: type, index: index)
81 | combinedAttr.append(NSAttributedString(string: "\t\(marker)\t", attributes: markerAttributes))
82 | combinedAttr.append(NSAttributedString(string: item, attributes: itemAttributes))
83 |
84 | if index < items.count - 1 {
85 | combinedAttr.append(NSAttributedString(string: "\n"))
86 | }
87 | }
88 |
89 | return combinedAttr
90 | }
91 |
92 | private static func markerWidth(for type: ListType, itemCount: Int, font: PlatformFont) -> CGFloat {
93 | // Calculate the maximum width of the markers
94 | switch type {
95 | case .ordered:
96 | var maxWidth: CGFloat = 0
97 | for index in 0.. maxWidth {
101 | maxWidth = width
102 | }
103 | }
104 | return maxWidth
105 | case .unordered:
106 | let marker = marker(for: type, index: 0)
107 | return marker.size(withAttributes: [.font: font]).width
108 | }
109 | }
110 |
111 | private static func marker(for type: ListType, index: Int) -> String {
112 | let marker: String
113 |
114 | switch type {
115 | case .ordered(let style):
116 | marker = MarkerGenerator.marker(for: style, number: index + 1)
117 | case .unordered(let style):
118 | marker = MarkerGenerator.marker(for: style)
119 | }
120 |
121 | return marker
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Example/Demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
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 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Example/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 52FFD7582D92A9A900AF5FE7 /* FormattedListKit in Frameworks */ = {isa = PBXBuildFile; productRef = 52FFD7572D92A9A900AF5FE7 /* FormattedListKit */; };
11 | 52FFD75A2D92A9B100AF5FE7 /* FormattedListKit in Frameworks */ = {isa = PBXBuildFile; productRef = 52FFD7592D92A9B100AF5FE7 /* FormattedListKit */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXFileReference section */
15 | 5221A3BE2D91657A004CFDD2 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
16 | 5245975A2D926541004486C0 /* MacDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
17 | /* End PBXFileReference section */
18 |
19 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
20 | 5221A3D02D91657C004CFDD2 /* Exceptions for "Demo" folder in "Demo" target */ = {
21 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
22 | membershipExceptions = (
23 | Info.plist,
24 | );
25 | target = 5221A3BD2D91657A004CFDD2 /* Demo */;
26 | };
27 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
28 |
29 | /* Begin PBXFileSystemSynchronizedRootGroup section */
30 | 5221A3C02D91657A004CFDD2 /* Demo */ = {
31 | isa = PBXFileSystemSynchronizedRootGroup;
32 | exceptions = (
33 | 5221A3D02D91657C004CFDD2 /* Exceptions for "Demo" folder in "Demo" target */,
34 | );
35 | path = Demo;
36 | sourceTree = "";
37 | };
38 | 5245975B2D926541004486C0 /* MacDemo */ = {
39 | isa = PBXFileSystemSynchronizedRootGroup;
40 | path = MacDemo;
41 | sourceTree = "";
42 | };
43 | /* End PBXFileSystemSynchronizedRootGroup section */
44 |
45 | /* Begin PBXFrameworksBuildPhase section */
46 | 5221A3BB2D91657A004CFDD2 /* Frameworks */ = {
47 | isa = PBXFrameworksBuildPhase;
48 | buildActionMask = 2147483647;
49 | files = (
50 | 52FFD7582D92A9A900AF5FE7 /* FormattedListKit in Frameworks */,
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | 524597572D926541004486C0 /* Frameworks */ = {
55 | isa = PBXFrameworksBuildPhase;
56 | buildActionMask = 2147483647;
57 | files = (
58 | 52FFD75A2D92A9B100AF5FE7 /* FormattedListKit in Frameworks */,
59 | );
60 | runOnlyForDeploymentPostprocessing = 0;
61 | };
62 | /* End PBXFrameworksBuildPhase section */
63 |
64 | /* Begin PBXGroup section */
65 | 5221A3B52D91657A004CFDD2 = {
66 | isa = PBXGroup;
67 | children = (
68 | 5221A3C02D91657A004CFDD2 /* Demo */,
69 | 5245975B2D926541004486C0 /* MacDemo */,
70 | 524597692D92656C004486C0 /* Frameworks */,
71 | 5221A3BF2D91657A004CFDD2 /* Products */,
72 | );
73 | sourceTree = "";
74 | };
75 | 5221A3BF2D91657A004CFDD2 /* Products */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 5221A3BE2D91657A004CFDD2 /* Demo.app */,
79 | 5245975A2D926541004486C0 /* MacDemo.app */,
80 | );
81 | name = Products;
82 | sourceTree = "";
83 | };
84 | 524597692D92656C004486C0 /* Frameworks */ = {
85 | isa = PBXGroup;
86 | children = (
87 | );
88 | name = Frameworks;
89 | sourceTree = "";
90 | };
91 | /* End PBXGroup section */
92 |
93 | /* Begin PBXNativeTarget section */
94 | 5221A3BD2D91657A004CFDD2 /* Demo */ = {
95 | isa = PBXNativeTarget;
96 | buildConfigurationList = 5221A3D12D91657C004CFDD2 /* Build configuration list for PBXNativeTarget "Demo" */;
97 | buildPhases = (
98 | 5221A3BA2D91657A004CFDD2 /* Sources */,
99 | 5221A3BB2D91657A004CFDD2 /* Frameworks */,
100 | 5221A3BC2D91657A004CFDD2 /* Resources */,
101 | );
102 | buildRules = (
103 | );
104 | dependencies = (
105 | );
106 | fileSystemSynchronizedGroups = (
107 | 5221A3C02D91657A004CFDD2 /* Demo */,
108 | );
109 | name = Demo;
110 | packageProductDependencies = (
111 | 52FFD7572D92A9A900AF5FE7 /* FormattedListKit */,
112 | );
113 | productName = Demo;
114 | productReference = 5221A3BE2D91657A004CFDD2 /* Demo.app */;
115 | productType = "com.apple.product-type.application";
116 | };
117 | 524597592D926541004486C0 /* MacDemo */ = {
118 | isa = PBXNativeTarget;
119 | buildConfigurationList = 524597662D926542004486C0 /* Build configuration list for PBXNativeTarget "MacDemo" */;
120 | buildPhases = (
121 | 524597562D926541004486C0 /* Sources */,
122 | 524597572D926541004486C0 /* Frameworks */,
123 | 524597582D926541004486C0 /* Resources */,
124 | );
125 | buildRules = (
126 | );
127 | dependencies = (
128 | );
129 | fileSystemSynchronizedGroups = (
130 | 5245975B2D926541004486C0 /* MacDemo */,
131 | );
132 | name = MacDemo;
133 | packageProductDependencies = (
134 | 52FFD7592D92A9B100AF5FE7 /* FormattedListKit */,
135 | );
136 | productName = MacDemo;
137 | productReference = 5245975A2D926541004486C0 /* MacDemo.app */;
138 | productType = "com.apple.product-type.application";
139 | };
140 | /* End PBXNativeTarget section */
141 |
142 | /* Begin PBXProject section */
143 | 5221A3B62D91657A004CFDD2 /* Project object */ = {
144 | isa = PBXProject;
145 | attributes = {
146 | BuildIndependentTargetsInParallel = 1;
147 | LastSwiftUpdateCheck = 1620;
148 | LastUpgradeCheck = 1620;
149 | TargetAttributes = {
150 | 5221A3BD2D91657A004CFDD2 = {
151 | CreatedOnToolsVersion = 16.2;
152 | };
153 | 524597592D926541004486C0 = {
154 | CreatedOnToolsVersion = 16.2;
155 | };
156 | };
157 | };
158 | buildConfigurationList = 5221A3B92D91657A004CFDD2 /* Build configuration list for PBXProject "Demo" */;
159 | developmentRegion = en;
160 | hasScannedForEncodings = 0;
161 | knownRegions = (
162 | en,
163 | Base,
164 | );
165 | mainGroup = 5221A3B52D91657A004CFDD2;
166 | minimizedProjectReferenceProxies = 1;
167 | packageReferences = (
168 | 52FFD7562D92A99C00AF5FE7 /* XCLocalSwiftPackageReference "../../FormattedListKit" */,
169 | );
170 | preferredProjectObjectVersion = 77;
171 | productRefGroup = 5221A3BF2D91657A004CFDD2 /* Products */;
172 | projectDirPath = "";
173 | projectRoot = "";
174 | targets = (
175 | 5221A3BD2D91657A004CFDD2 /* Demo */,
176 | 524597592D926541004486C0 /* MacDemo */,
177 | );
178 | };
179 | /* End PBXProject section */
180 |
181 | /* Begin PBXResourcesBuildPhase section */
182 | 5221A3BC2D91657A004CFDD2 /* Resources */ = {
183 | isa = PBXResourcesBuildPhase;
184 | buildActionMask = 2147483647;
185 | files = (
186 | );
187 | runOnlyForDeploymentPostprocessing = 0;
188 | };
189 | 524597582D926541004486C0 /* Resources */ = {
190 | isa = PBXResourcesBuildPhase;
191 | buildActionMask = 2147483647;
192 | files = (
193 | );
194 | runOnlyForDeploymentPostprocessing = 0;
195 | };
196 | /* End PBXResourcesBuildPhase section */
197 |
198 | /* Begin PBXSourcesBuildPhase section */
199 | 5221A3BA2D91657A004CFDD2 /* Sources */ = {
200 | isa = PBXSourcesBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | );
204 | runOnlyForDeploymentPostprocessing = 0;
205 | };
206 | 524597562D926541004486C0 /* Sources */ = {
207 | isa = PBXSourcesBuildPhase;
208 | buildActionMask = 2147483647;
209 | files = (
210 | );
211 | runOnlyForDeploymentPostprocessing = 0;
212 | };
213 | /* End PBXSourcesBuildPhase section */
214 |
215 | /* Begin XCBuildConfiguration section */
216 | 5221A3D22D91657C004CFDD2 /* Debug */ = {
217 | isa = XCBuildConfiguration;
218 | buildSettings = {
219 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
220 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
221 | CODE_SIGN_STYLE = Automatic;
222 | CURRENT_PROJECT_VERSION = 1;
223 | GENERATE_INFOPLIST_FILE = YES;
224 | INFOPLIST_FILE = Demo/Info.plist;
225 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
226 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
227 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
228 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
229 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
230 | IPHONEOS_DEPLOYMENT_TARGET = 16.6;
231 | LD_RUNPATH_SEARCH_PATHS = (
232 | "$(inherited)",
233 | "@executable_path/Frameworks",
234 | );
235 | MARKETING_VERSION = 1.0;
236 | PRODUCT_BUNDLE_IDENTIFIER = com.nelson.Demo;
237 | PRODUCT_NAME = "$(TARGET_NAME)";
238 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
239 | SUPPORTS_MACCATALYST = NO;
240 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
241 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
242 | SWIFT_EMIT_LOC_STRINGS = YES;
243 | SWIFT_VERSION = 5.0;
244 | TARGETED_DEVICE_FAMILY = 1;
245 | };
246 | name = Debug;
247 | };
248 | 5221A3D32D91657C004CFDD2 /* Release */ = {
249 | isa = XCBuildConfiguration;
250 | buildSettings = {
251 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
252 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
253 | CODE_SIGN_STYLE = Automatic;
254 | CURRENT_PROJECT_VERSION = 1;
255 | GENERATE_INFOPLIST_FILE = YES;
256 | INFOPLIST_FILE = Demo/Info.plist;
257 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
258 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
259 | INFOPLIST_KEY_UIMainStoryboardFile = Main;
260 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
261 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
262 | IPHONEOS_DEPLOYMENT_TARGET = 16.6;
263 | LD_RUNPATH_SEARCH_PATHS = (
264 | "$(inherited)",
265 | "@executable_path/Frameworks",
266 | );
267 | MARKETING_VERSION = 1.0;
268 | PRODUCT_BUNDLE_IDENTIFIER = com.nelson.Demo;
269 | PRODUCT_NAME = "$(TARGET_NAME)";
270 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
271 | SUPPORTS_MACCATALYST = NO;
272 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
273 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
274 | SWIFT_EMIT_LOC_STRINGS = YES;
275 | SWIFT_VERSION = 5.0;
276 | TARGETED_DEVICE_FAMILY = 1;
277 | };
278 | name = Release;
279 | };
280 | 5221A3D42D91657C004CFDD2 /* Debug */ = {
281 | isa = XCBuildConfiguration;
282 | buildSettings = {
283 | ALWAYS_SEARCH_USER_PATHS = NO;
284 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
285 | CLANG_ANALYZER_NONNULL = YES;
286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
288 | CLANG_ENABLE_MODULES = YES;
289 | CLANG_ENABLE_OBJC_ARC = YES;
290 | CLANG_ENABLE_OBJC_WEAK = YES;
291 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
292 | CLANG_WARN_BOOL_CONVERSION = YES;
293 | CLANG_WARN_COMMA = YES;
294 | CLANG_WARN_CONSTANT_CONVERSION = YES;
295 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
296 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
297 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
298 | CLANG_WARN_EMPTY_BODY = YES;
299 | CLANG_WARN_ENUM_CONVERSION = YES;
300 | CLANG_WARN_INFINITE_RECURSION = YES;
301 | CLANG_WARN_INT_CONVERSION = YES;
302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
306 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
308 | CLANG_WARN_STRICT_PROTOTYPES = YES;
309 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
311 | CLANG_WARN_UNREACHABLE_CODE = YES;
312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
313 | COPY_PHASE_STRIP = NO;
314 | DEBUG_INFORMATION_FORMAT = dwarf;
315 | ENABLE_STRICT_OBJC_MSGSEND = YES;
316 | ENABLE_TESTABILITY = YES;
317 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
318 | GCC_C_LANGUAGE_STANDARD = gnu17;
319 | GCC_DYNAMIC_NO_PIC = NO;
320 | GCC_NO_COMMON_BLOCKS = YES;
321 | GCC_OPTIMIZATION_LEVEL = 0;
322 | GCC_PREPROCESSOR_DEFINITIONS = (
323 | "DEBUG=1",
324 | "$(inherited)",
325 | );
326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
328 | GCC_WARN_UNDECLARED_SELECTOR = YES;
329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
330 | GCC_WARN_UNUSED_FUNCTION = YES;
331 | GCC_WARN_UNUSED_VARIABLE = YES;
332 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
333 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
334 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
335 | MTL_FAST_MATH = YES;
336 | ONLY_ACTIVE_ARCH = YES;
337 | SDKROOT = iphoneos;
338 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
339 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
340 | };
341 | name = Debug;
342 | };
343 | 5221A3D52D91657C004CFDD2 /* Release */ = {
344 | isa = XCBuildConfiguration;
345 | buildSettings = {
346 | ALWAYS_SEARCH_USER_PATHS = NO;
347 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
348 | CLANG_ANALYZER_NONNULL = YES;
349 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
350 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
351 | CLANG_ENABLE_MODULES = YES;
352 | CLANG_ENABLE_OBJC_ARC = YES;
353 | CLANG_ENABLE_OBJC_WEAK = YES;
354 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
355 | CLANG_WARN_BOOL_CONVERSION = YES;
356 | CLANG_WARN_COMMA = YES;
357 | CLANG_WARN_CONSTANT_CONVERSION = YES;
358 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
359 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
360 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
361 | CLANG_WARN_EMPTY_BODY = YES;
362 | CLANG_WARN_ENUM_CONVERSION = YES;
363 | CLANG_WARN_INFINITE_RECURSION = YES;
364 | CLANG_WARN_INT_CONVERSION = YES;
365 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
366 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
367 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
368 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
369 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
370 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
371 | CLANG_WARN_STRICT_PROTOTYPES = YES;
372 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
373 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
374 | CLANG_WARN_UNREACHABLE_CODE = YES;
375 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
376 | COPY_PHASE_STRIP = NO;
377 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
378 | ENABLE_NS_ASSERTIONS = NO;
379 | ENABLE_STRICT_OBJC_MSGSEND = YES;
380 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
381 | GCC_C_LANGUAGE_STANDARD = gnu17;
382 | GCC_NO_COMMON_BLOCKS = YES;
383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
385 | GCC_WARN_UNDECLARED_SELECTOR = YES;
386 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
387 | GCC_WARN_UNUSED_FUNCTION = YES;
388 | GCC_WARN_UNUSED_VARIABLE = YES;
389 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
390 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
391 | MTL_ENABLE_DEBUG_INFO = NO;
392 | MTL_FAST_MATH = YES;
393 | SDKROOT = iphoneos;
394 | SWIFT_COMPILATION_MODE = wholemodule;
395 | VALIDATE_PRODUCT = YES;
396 | };
397 | name = Release;
398 | };
399 | 524597672D926542004486C0 /* Debug */ = {
400 | isa = XCBuildConfiguration;
401 | buildSettings = {
402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
403 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
404 | CODE_SIGN_ENTITLEMENTS = MacDemo/MacDemo.entitlements;
405 | CODE_SIGN_STYLE = Automatic;
406 | COMBINE_HIDPI_IMAGES = YES;
407 | CURRENT_PROJECT_VERSION = 1;
408 | GENERATE_INFOPLIST_FILE = YES;
409 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
410 | INFOPLIST_KEY_NSMainStoryboardFile = Main;
411 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
412 | LD_RUNPATH_SEARCH_PATHS = (
413 | "$(inherited)",
414 | "@executable_path/../Frameworks",
415 | );
416 | MACOSX_DEPLOYMENT_TARGET = 13.5;
417 | MARKETING_VERSION = 1.0;
418 | PRODUCT_BUNDLE_IDENTIFIER = com.nelson.MacDemo;
419 | PRODUCT_NAME = "$(TARGET_NAME)";
420 | SDKROOT = macosx;
421 | SWIFT_EMIT_LOC_STRINGS = YES;
422 | SWIFT_VERSION = 5.0;
423 | };
424 | name = Debug;
425 | };
426 | 524597682D926542004486C0 /* Release */ = {
427 | isa = XCBuildConfiguration;
428 | buildSettings = {
429 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
430 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
431 | CODE_SIGN_ENTITLEMENTS = MacDemo/MacDemo.entitlements;
432 | CODE_SIGN_STYLE = Automatic;
433 | COMBINE_HIDPI_IMAGES = YES;
434 | CURRENT_PROJECT_VERSION = 1;
435 | GENERATE_INFOPLIST_FILE = YES;
436 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
437 | INFOPLIST_KEY_NSMainStoryboardFile = Main;
438 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
439 | LD_RUNPATH_SEARCH_PATHS = (
440 | "$(inherited)",
441 | "@executable_path/../Frameworks",
442 | );
443 | MACOSX_DEPLOYMENT_TARGET = 13.5;
444 | MARKETING_VERSION = 1.0;
445 | PRODUCT_BUNDLE_IDENTIFIER = com.nelson.MacDemo;
446 | PRODUCT_NAME = "$(TARGET_NAME)";
447 | SDKROOT = macosx;
448 | SWIFT_EMIT_LOC_STRINGS = YES;
449 | SWIFT_VERSION = 5.0;
450 | };
451 | name = Release;
452 | };
453 | /* End XCBuildConfiguration section */
454 |
455 | /* Begin XCConfigurationList section */
456 | 5221A3B92D91657A004CFDD2 /* Build configuration list for PBXProject "Demo" */ = {
457 | isa = XCConfigurationList;
458 | buildConfigurations = (
459 | 5221A3D42D91657C004CFDD2 /* Debug */,
460 | 5221A3D52D91657C004CFDD2 /* Release */,
461 | );
462 | defaultConfigurationIsVisible = 0;
463 | defaultConfigurationName = Release;
464 | };
465 | 5221A3D12D91657C004CFDD2 /* Build configuration list for PBXNativeTarget "Demo" */ = {
466 | isa = XCConfigurationList;
467 | buildConfigurations = (
468 | 5221A3D22D91657C004CFDD2 /* Debug */,
469 | 5221A3D32D91657C004CFDD2 /* Release */,
470 | );
471 | defaultConfigurationIsVisible = 0;
472 | defaultConfigurationName = Release;
473 | };
474 | 524597662D926542004486C0 /* Build configuration list for PBXNativeTarget "MacDemo" */ = {
475 | isa = XCConfigurationList;
476 | buildConfigurations = (
477 | 524597672D926542004486C0 /* Debug */,
478 | 524597682D926542004486C0 /* Release */,
479 | );
480 | defaultConfigurationIsVisible = 0;
481 | defaultConfigurationName = Release;
482 | };
483 | /* End XCConfigurationList section */
484 |
485 | /* Begin XCLocalSwiftPackageReference section */
486 | 52FFD7562D92A99C00AF5FE7 /* XCLocalSwiftPackageReference "../../FormattedListKit" */ = {
487 | isa = XCLocalSwiftPackageReference;
488 | relativePath = ../../FormattedListKit;
489 | };
490 | /* End XCLocalSwiftPackageReference section */
491 |
492 | /* Begin XCSwiftPackageProductDependency section */
493 | 52FFD7572D92A9A900AF5FE7 /* FormattedListKit */ = {
494 | isa = XCSwiftPackageProductDependency;
495 | package = 52FFD7562D92A99C00AF5FE7 /* XCLocalSwiftPackageReference "../../FormattedListKit" */;
496 | productName = FormattedListKit;
497 | };
498 | 52FFD7592D92A9B100AF5FE7 /* FormattedListKit */ = {
499 | isa = XCSwiftPackageProductDependency;
500 | package = 52FFD7562D92A99C00AF5FE7 /* XCLocalSwiftPackageReference "../../FormattedListKit" */;
501 | productName = FormattedListKit;
502 | };
503 | /* End XCSwiftPackageProductDependency section */
504 | };
505 | rootObject = 5221A3B62D91657A004CFDD2 /* Project object */;
506 | }
507 |
--------------------------------------------------------------------------------
/Example/MacDemo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
--------------------------------------------------------------------------------