Earlier this year ago I wrote a post about using the SF Font symbols on macOS Picture.SystemImage
in iOS apps. However that technique has some downsides. For one, the symbol glyphs are hardcoded, which means that it’s not possible to access the new symbols added to the SF Font by Apple. In addition, it isn’t possible to set the font weight and scale for the glyph. In this new post, I’ll show a more flexible way to work with these symbols on macOS 11+.
This technique is more on par with the current Picture.SystemImage
you use in your Xojo iOS apps. With it you will be able to set the following attributes:
- Glyph name. You can use Apple’s Symbols SF utility to view all the available symbols and the name associated with each of these.
- Size (in points)
- Weight. The weight of the SF Font, ranging from Ultra-Light to Black. This is an Enumeration value.
- Scale. The scale of the SF Font symbol, ranging from Small to Large. This is an Enumeration value.
- TemplateColor. If you want to tint the resulting glyph, you can provide a ColorGroup parameter (Desktop apps) or a regular Color parameter (Desktop, Console apps).
- FallbackTemplateImage. You can provide a grayscale / black and white image to use as a fallback image, in case there is no way to create a picture from the received “name” string.
As it is the case with the existing iOS method, you will receive a Picture. For example you will be able to use code like this in your desktop projects:
Var c As New ColorGroup(Color.Red, Color.White) Var p As Picture = SystemImage("highlighter", 120.0, SystemImageWeights.UltraLight, Symbolscale.Small, c, fallback) IVSFGlyph.Image = p
The “fallback” parameter refers to an image added to a desktop project that will be used if the glyph is not found. This will produce the following glyph, assigned as the image to the ImageView
control named IVSFGlyph
:
In order to add this kind of functionality to your Xojo Desktop and Console projects on macOS, add a new Module to the Navigator (for example, with the name macOSLib). Then, with the macOSLib item selected in the Navigator, add two enumerations with the following values:
- Name: SymbolScale
- Type: Integer
- Scope: Global
- Values:
Small = 1
Medium = 2
Large = 3
- Name: SystemImageWeights
- Type: Integer
- Values:
UltraLight = 0
Thin = 1
Light = 2
Regular = 3
Medium = 4
Semibold = 5
Bold = 6
Heavy = 7
Black = 8
Now add to the macOSLib
module the method that will run both on Desktop and Console macOS apps:
- Method Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As SymbolScale = SymbolScale.Medium, templateColor As Color, fallbackTemplateImage As Picture = Nil
- Return Type: Picture
- Scope: Global
Click on the cog wheel icon in the Inspector to change to the attributes section for the method, and make sure to set only the options displayed in the following picture:
Finally, put the following code in the Code Editor associated with the newly created method:
#If TargetMacOS If System.Version >= "11.0" Then If name = "" Then Return Nil Declare Function Alloc Lib "Foundation" Selector "alloc" (classRef As Ptr) As Ptr Declare Sub AutoRelease Lib "Foundation" Selector "autorelease" (classInstance As Ptr) Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As Ptr Declare Function ImageWithSystemSymbolName Lib "AppKit" Selector "imageWithSystemSymbolName:accessibilityDescription:" (imgClass As Ptr, symbolName As CFStringRef, accesibility As CFStringRef) As Ptr Declare Function ConfigurationWithPointSize Lib "AppKit" Selector "configurationWithPointSize:weight:scale:" (symbolConfClass As Ptr, size As CGFloat, weight As CGFloat, tscale As SymbolScale) As Ptr Declare Function ImageWithSymbolConfiguration Lib "AppKit" Selector "imageWithSymbolConfiguration:" (imgClass As Ptr, config As Ptr) As Ptr Declare Function ColorWithRGBA Lib "Foundation" Selector "colorWithRed:green:blue:alpha:" (nscolor As ptr, red As CGFloat, green As CGFloat, blue As CGFloat, alpha As CGFloat) As Ptr Declare Sub SetTemplate Lib "AppKit" Selector "setTemplate:" (imageObj As Ptr, value As Boolean) Declare Sub LockFocus Lib "AppKit" Selector "lockFocus" (imageObj As Ptr) Declare Sub UnlockFocus Lib "AppKit" Selector "unlockFocus" (imageObj As Ptr) Declare Sub Set Lib "Foundation" Selector "set" (colorObj As Ptr) Declare Sub NSRectFillUsingOperation Lib "AppKit" (rect As NSRect, option As UInteger) Declare Function RepresentationUsingType Lib "AppKit" Selector "representationUsingType:properties:" (imageRep As Ptr, type As UInteger, properties As Ptr) As Ptr Declare Function InitWithFocusedView Lib "AppKit" Selector "initWithFocusedViewRect:" (imageObj As Ptr, rect As NSRect) As Ptr Var nsimage As Ptr = NSClassFromString("NSImage") Var orImage As Ptr = ImageWithSystemSymbolName(nsimage, name, "") Var symbolConfClass As Ptr = NSClassFromString("NSImageSymbolConfiguration") // Getting the weight as the required SystemImageWeight float Var tWeight As CGFloat = SystemImageWeight(weight) // Creating a configuration obj for the Glyph Var symbolConf As Ptr = ConfigurationWithPointSize(symbolConfClass, size, tWeight, scale) // Getting the final NSImage from the Glyph + Conf (still in vectorial format) Var finalImage As Ptr = ImageWithSymbolConfiguration(orImage, symbolConf) // Can't create image from received glyph name, so we return Nil if fallback is not provided // or colorize the fallback image if there is one If finalImage = Nil Then If fallbackTemplateImage = Nil Then Return Nil Var fallbackData As MemoryBlock = fallbackTemplateImage.ToData(Picture.Formats.PNG) Var fallbackDataPtr As Ptr = fallbackData Declare Function DataWithBytesLength Lib "Foundation" Selector "dataWithBytes:length:" (dataClass As Ptr, data As Ptr, length As UInteger) As Ptr If fallbackData <> Nil And fallbackData.Size > 0 Then Var NSDataClass As Ptr = NSClassFromString("NSData") Var NSDataObj As Ptr = DataWithBytesLength(NSDataclass, fallbackDataPtr, fallbackData.Size) If NSDataObj <> Nil Then Declare Function InitWithData Lib "AppKit" Selector "initWithData:" (imageInstance As Ptr, data As Ptr) As Ptr Var NSImageClass As Ptr = NSClassFromString("NSImage") finalImage = Alloc(NSImageClass) finalImage = InitWithData(finalImage, NSDataObj) AutoRelease(NSDataObj) End If End If End If If finalImage = Nil Then Return Nil Var c As Color Var nscolor As Ptr LockFocus(finalImage) // Applying tint to the image if we receive a valid ColorGroup object c = templateColor nscolor = NSClassFromString("NSColor") Var tColor As Ptr = ColorWithRGBA(nscolor, c.Red/255.0, c.Green/255.0, c.Blue/255.0, 1.0-c.Alpha/255.0) // We need to set the Template property of the NSImage to False in order to colorize it. SetTemplate(finalImage, False) Declare Function ImageSize Lib "AppKit" Selector "size" (imageObjt As Ptr) As NSSize Var tRect As NSRect tRect.Origin.X = 0 tRect.Origin.Y = 0 tRect.RectSize = ImageSize(finalImage) Set(tColor) NSRectFillUsingOperation(tRect, 3) // Getting bitmap image representation in order to extract the data as PNG. Var NSBitmapImageRepClass As Ptr = NSClassFromString("NSBitmapImageRep") Var NSBitmapImageRepInstance As Ptr = Alloc(NSBitmapImageRepClass) Var newRep As Ptr = InitWithFocusedView(NSBitmapImageRepInstance, tRect) UnlockFocus(finalImage) Var data As Ptr = RepresentationUsingType(newRep, 4, Nil) // 4 = PNG AutoRelease(newRep) AutoRelease(nscolor) // Getting image data to generate the Picture object in the Xojo side Declare Function DataLength Lib "Foundation" Selector "length" (obj As Ptr) As Integer Declare Sub GetDataBytes Lib "Foundation" Selector "getBytes:length:" (obj As Ptr, buff As ptr, len As Integer) // We need to get the length of the raw data… Var dlen As Integer = DataLength(data) // …in order to create a memoryblock with the right size Var mb As New MemoryBlock(dlen) Var mbPtr As Ptr = mb // And now we can dump the PNG data from the NSDATA objecto to the memoryblock GetDataBytes(data, mbPtr, dlen) // In order to create a Xojo Picture from it Return Picture.FromData(mb) End If #EndIf
As you can see, this method calls the SystemImageWeight
method, and also makes use of some Structs
. Let’s add to the method that will convert the received enum value to the required CGFloat
value needed by the Declare:
- Method Name: SystemImageWeight
- Parameters: weight As SystemImageWeights
- Returned Type: CGFloat
- Scope: Global
Add the following code in the associated Code Editor:
Var tWeight As CGFloat = 0 Select Case weight Case SystemImageWeights.UltraLight tWeight = -1.0 Case SystemImageWeights.Thin tWeight = -0.75 Case SystemImageWeights.Light tWeight = -0.5 Case SystemImageWeights.Regular tWeight = -0.25 Case SystemImageWeights.Medium tWeight = 0 Case SystemImageWeights.Semibold tWeight = 0.25 Case SystemImageWeights.Bold tWeight = 0.5 Case SystemImageWeights.Heavy tWeight = 0.75 Case SystemImageWeights.Black tWeight = 1 End Select Return tWeight
And now add three new Structures to the macOSLib module using the following values:
- Structure Name: NSOrigin
- Scope: Global
X as CGFloat
Y as CGFloat
- Structure Name: NSSize
- Scope: Global
Height as CGFloat
Width as CGFloat
- Structure Name: NSRect
- Scope: Global
Origin as NSOrigin
RectSize as NSSize
Add an overloaded method only intended for macOS desktop apps. The main difference is that this one can receive a ColorGroup
parameter instead of just a Color
object:
- Method Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As SymbolScale = SymbolScale.Medium, templateColor As ColorGroup = Nil, fallbackTemplateImage As Picture = Nil
- Returned Type: Picture
- Scope: Global
And type the following code in the associated Code Editor for the method:
Var c As Color If templateColor <> Nil Then c = templateColor End If Return SystemImage(name, size, weight, scale, c, fallbackTemplateImage)
Click on the cog wheel icon from the method Inspector and set the attributes as shown in the following picture:
Setting SF Glyphs directly to Views
Let’s add now a third method to the module whose main difference is that it will not return a Picture; instead, it will set the specified SF glyph as the image for the received control handler. For example, this is neat if you want to set a SF symbol to a button in you UI interface (among other UI controls).
This is the signature for the new method:
- Name: SystemImage
- Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As SymbolScale = SymbolScale.Small, controlHandler As Integer
- Scope: Global
And the code to put in the associated Code Editor:
#If TargetMacOS If System.Version >= "11.0" Then If name = "" Then Exit Declare Function Alloc Lib "Foundation" Selector "alloc" (classRef As Ptr) As Ptr Declare Sub AutoRelease Lib "Foundation" Selector "autorelease" (classInstance As Ptr) Declare Function NSClassFromString Lib "Foundation" (clsName As CFStringRef) As Ptr Declare Function ImageWithSystemSymbolName Lib "AppKit" Selector "imageWithSystemSymbolName:accessibilityDescription:" (imgClass As Ptr, symbolName As CFStringRef, accesibility As CFStringRef) As Ptr Declare Function ConfigurationWithPointSize Lib "AppKit" Selector "configurationWithPointSize:weight:scale:" (symbolConfClass As Ptr, size As CGFloat, weight As CGFloat, scale As SymbolScale) As Ptr Declare Function ImageWithSymbolConfiguration Lib "AppKit" Selector "imageWithSymbolConfiguration:" (imgClass As Ptr, config As Ptr) As Ptr Var nsimage As Ptr = NSClassFromString("NSImage") Var orImage As Ptr = ImageWithSystemSymbolName(nsimage, name,"") Var symbolConfClass As Ptr = NSClassFromString("NSImageSymbolConfiguration") // Getting the weight as the required SystemImageWeight float Var tWeight As CGFloat = SystemImageWeight(weight) // Creating a configuration obj for the Glyph Var symbolConf As Ptr = ConfigurationWithPointSize(symbolConfClass, size, tWeight, scale) // Getting the final NSImage from the Glyph + Conf (still in vectorial format) Var finalImage As Ptr = ImageWithSymbolConfiguration(orImage, symbolConf) // We need to know if the received Handler can respond to the setImage message (that is, it's a View) Declare Function RespondsToSelector Lib "/usr/lib/libobjc.A.dylib" Selector "respondsToSelector:" (obj As Integer, sel As Ptr) As Boolean Declare Function NSSelectorFromString Lib "Foundation" (sel As CFStringRef) As Ptr Var sel As Ptr = NSSelectorFromString("setImage:") // We check if it's a valid handler, we have an NSImage object and the handler can receive the "setImage" message If controlHandler <> 0 And finalImage <> Nil And RespondsToSelector(controlHandler, sel) Then Declare Sub Set Lib "AppKit" Selector "setImage:" (control As Integer, Image As Ptr) // We set the NSImage to the received control Set(controlHandler, finalImage) End If End If #EndIf
So, for example, you can set now the Gear symbol as the image of a PushButton
control added to the app UI using the following code in the Open
Event Handler of the PushButton
instance:
SystemImage("gearshape.2", 14.0, SystemImageWeights.Regular, SymbolScale.Small, Me.Handle)
Summary
And that’s all! As you can see, there are no hardcoded codepoints for the SF glyphs. You’ll be able to create a picture from any SF symbol at the desired points size, weight and scale. Plus, you’ll be able to colorize it and receive a fallback image if anything goes wrong in the process.
You can download the example Xojo project with the Module already created from this link.