@@ -10,84 +10,141 @@ import SwiftUI
10
10
struct MonospacedFontPicker : View {
11
11
var title : String
12
12
@Binding var selectedFontName : String
13
- @State var recentFonts : [ String ]
14
- @State var monospacedFontFamilyNames : [ String ] = [ ]
15
- @State var otherFontFamilyNames : [ String ] = [ ]
13
+ @State private var recentFonts : [ String ]
14
+ @State private var monospacedFontFamilyNames : [ String ] = [ ]
15
+ @State private var otherFontFamilyNames : [ String ] = [ ]
16
16
17
17
init ( title: String , selectedFontName: Binding < String > ) {
18
18
self . title = title
19
19
self . _selectedFontName = selectedFontName
20
20
self . recentFonts = UserDefaults . standard. stringArray ( forKey: " recentFonts " ) ?? [ ]
21
21
}
22
22
23
- private func pushIntoRecentFonts( _ newItem: String ) {
24
- recentFonts. removeAll ( where: { $0 == newItem } )
25
- recentFonts. insert ( newItem, at: 0 )
26
- if recentFonts. count > 3 {
27
- recentFonts. removeLast ( )
28
- }
29
- UserDefaults . standard. set ( recentFonts, forKey: " recentFonts " )
30
- }
31
-
32
- func getFonts( ) {
33
- DispatchQueue . global ( qos: . userInitiated) . async {
34
- let availableFontFamilies = NSFontManager . shared. availableFontFamilies
35
-
36
- self . monospacedFontFamilyNames = availableFontFamilies. filter { fontFamilyName in
37
- let fontNames = NSFontManager . shared. availableMembers ( ofFontFamily: fontFamilyName) ?? [ ]
38
- return fontNames. contains { fontName in
39
- guard let font = NSFont ( name: " \( fontName [ 0 ] ) " , size: 14 ) else {
40
- return false
41
- }
42
- return font. isFixedPitch && font. numberOfGlyphs > 26
43
- }
44
- }
45
- . filter { $0 != " SF Mono " }
46
-
47
- self . otherFontFamilyNames = availableFontFamilies. filter { fontFamilyName in
48
- let fontNames = NSFontManager . shared. availableMembers ( ofFontFamily: fontFamilyName) ?? [ ]
49
- return fontNames. contains { fontName in
50
- guard let font = NSFont ( name: " \( fontName [ 0 ] ) " , size: 14 ) else {
51
- return false
52
- }
53
- return !font. isFixedPitch && font. numberOfGlyphs > 26
54
- }
55
- }
56
- }
57
- }
58
-
59
23
var body : some View {
60
- return Picker ( selection: $selectedFontName, label: Text ( title) ) {
24
+ Picker ( selection: $selectedFontName, label: Text ( title) ) {
61
25
Text ( " System Font " )
62
26
. font ( Font ( NSFont . monospacedSystemFont ( ofSize: 13.5 , weight: . medium) ) )
63
27
. tag ( " SF Mono " )
28
+
64
29
if !recentFonts. isEmpty {
65
30
Divider ( )
66
31
ForEach ( recentFonts, id: \. self) { fontFamilyName in
67
- Text ( fontFamilyName) . font ( . custom( fontFamilyName, size: 13.5 ) )
32
+ Text ( fontFamilyName)
33
+ . font ( . custom( fontFamilyName, size: 13.5 ) )
34
+ . tag ( fontFamilyName) // to prevent picker invalid and does not have an associated tag error.
68
35
}
69
36
}
37
+
70
38
if !monospacedFontFamilyNames. isEmpty {
71
39
Divider ( )
72
40
ForEach ( monospacedFontFamilyNames, id: \. self) { fontFamilyName in
73
- Text ( fontFamilyName) . font ( . custom( fontFamilyName, size: 13.5 ) )
41
+ Text ( fontFamilyName)
42
+ . font ( . custom( fontFamilyName, size: 13.5 ) )
43
+ . tag ( fontFamilyName)
74
44
}
75
45
}
46
+
76
47
if !otherFontFamilyNames. isEmpty {
77
48
Divider ( )
78
- Picker ( selection : $selectedFontName , label : Text ( " Other fonts... " ) ) {
49
+ Menu {
79
50
ForEach ( otherFontFamilyNames, id: \. self) { fontFamilyName in
80
- Text ( fontFamilyName)
81
- . font ( . custom( fontFamilyName, size: 13.5 ) )
51
+ Button {
52
+ pushIntoRecentFonts ( fontFamilyName)
53
+ selectedFontName = fontFamilyName
54
+ } label: {
55
+ Text ( fontFamilyName)
56
+ . font ( . custom( fontFamilyName, size: 13.5 ) )
57
+ }
58
+ . tag ( fontFamilyName)
82
59
}
60
+ } label: {
61
+ Text ( " Other fonts... " )
83
62
}
84
63
}
85
64
}
86
65
. onChange ( of: selectedFontName) { _ in
87
66
if selectedFontName != " SF Mono " {
88
67
pushIntoRecentFonts ( selectedFontName)
68
+
69
+ // remove the font to prevent ForEach conflict
70
+ monospacedFontFamilyNames. removeAll { $0 == selectedFontName }
71
+ otherFontFamilyNames. removeAll { $0 == selectedFontName }
72
+ }
73
+ }
74
+ . task {
75
+ await getFonts ( )
76
+ }
77
+ }
78
+ }
79
+
80
+ extension MonospacedFontPicker {
81
+ private func pushIntoRecentFonts( _ newItem: String ) {
82
+ recentFonts. removeAll ( where: { $0 == newItem } )
83
+ recentFonts. insert ( newItem, at: 0 )
84
+ if recentFonts. count > 3 {
85
+ recentFonts. removeLast ( )
86
+ }
87
+ UserDefaults . standard. set ( recentFonts, forKey: " recentFonts " )
88
+ }
89
+
90
+ private func getFonts( ) async {
91
+ await withTaskGroup ( of: Void . self) { group in
92
+ group. addTask {
93
+ let monospacedFontFamilyNames = getMonospacedFamilyNames ( )
94
+ await MainActor . run {
95
+ self . monospacedFontFamilyNames = monospacedFontFamilyNames
96
+ }
97
+ }
98
+
99
+ group. addTask {
100
+ let otherFontFamilyNames = getOtherFontFamilyNames ( )
101
+ await MainActor . run {
102
+ self . otherFontFamilyNames = otherFontFamilyNames
103
+ }
89
104
}
90
105
}
91
- . onAppear ( perform: getFonts)
92
106
}
107
+
108
+ private func getMonospacedFamilyNames( ) -> [ String ] {
109
+ let availableFontFamilies = NSFontManager . shared. availableFontFamilies
110
+
111
+ return availableFontFamilies. filter { fontFamilyName in
112
+ // exclude the font if it is in recentFonts to prevent ForEach conflict
113
+ if recentFonts. contains ( fontFamilyName) {
114
+ return false
115
+ }
116
+
117
+ // exclude default font
118
+ if fontFamilyName == " SF Mono " {
119
+ return false
120
+ }
121
+
122
+ // include the font which is fixedPitch
123
+ // include the font which numberOfGlyphs is greater than 26
124
+ if let font = NSFont ( name: fontFamilyName, size: 14 ) {
125
+ return font. isFixedPitch && font. numberOfGlyphs > 26
126
+ } else {
127
+ return false
128
+ }
129
+ }
130
+ }
131
+
132
+ private func getOtherFontFamilyNames( ) -> [ String ] {
133
+ let availableFontFamilies = NSFontManager . shared. availableFontFamilies
134
+
135
+ return availableFontFamilies. filter { fontFamilyName in
136
+ // exclude the font if it is in recentFonts to prevent ForEach conflict
137
+ if recentFonts. contains ( fontFamilyName) {
138
+ return false
139
+ }
140
+
141
+ // include the font which is NOT fixedPitch
142
+ // include the font which numberOfGlyphs is greater than 26
143
+ if let font = NSFont ( name: fontFamilyName, size: 14 ) {
144
+ return !font. isFixedPitch && font. numberOfGlyphs > 26
145
+ } else {
146
+ return false
147
+ }
148
+ }
149
+ }
93
150
}
0 commit comments