Truco: Usar los símbolos SF en macOS… revisado

Hace algunos meses publicamos una técnica para utilizar los símbolos de la fuente SF en macOS tal y como es posible cuando se utiliza el método Picture.SystemImage en las apps de iOS. Sin embargo dicha técnica presentaba algunas desventajas: los glifos de los símbolos estaban definidos directamente en código, lo que significa que no es posible acceder a los nuevos símbolos añadidos de vez en cuando por Apple a la fuente SF. Además, tampoco era posible definir algunas propiedades como por ejemplo el peso o la escala del glifo. Continúa leyendo y te mostraré una forma más flexible de utilizar estos símbolos en macOS 11+.

De hecho, la nueva técnica propuesta es más parecida al funcionamiento del actual método Picture.SystemImage que puedes usar en tus apps iOS de Xojo. Así, podrás definir los siguientes atributos:

  • Nombre del Glifo. Puedes usar la utilidad Symbols SF de Apple para ver todos los símbolos disponibles así como el nombre asociado con cada uno de ellos.
  • Tamaño (en puntos)
  • Peso. El peso a utilizar en la fuente SF, comprendiendo desde Ultra-Light a Black. Este es un tipo de dato Enumerador.
  • Escala. La escala del símbolo, comprendiendo de Small (pequeño) a Large (grande). Este es un valor de tipo Enumerador.
  • TemplateColor. Si quieres tintar (colorear) el glifo resultante, entonces puedes proporcionar un parámetro ColorGroup (en las apps Desktop) o bien un valor de tipo de dato Color (apps Desktop y Consola).
  • FallbackTemplateImage. Puedes proporcionar una imagen en escala de grises o blanco y negro a usar como imagen de respaldo en el caso de que no se pueda generar un Picture a partir del nombre de glifo proporcionado.

Tal y como ocurre en el actual método de iOS, recibirás una instancia de Picture. Por ejemplo:

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

Siendo en este caso “fallback” una imagen que se ha añadido previamente al proyecto Desktop. Este fragmento de código resultará en el siguiente glifo asignado como imagen al control ImageView con el nombre IVSFGlyph:

Para añadir este tipo de funcionalidad a tus proyectos Xojo Desktop y de Consola en macOS, añade un nuevo Módulo al Navegador (por ejemplo, usando el nombre macOSLib). A continuación, y con el ítem macOSLib seleccionado en el Navegador, añade dos enumeradores con los siguientes valores:

  • 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

Añadamos ahora al módulo macOSLib el método que funcionará tanto en aplicaciones Desktop como de Consola en macOS:

  • 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

Haz clic en el icono de la rueda dentada en el Inspector para cambiar a la sección de atributos para dicho método, y asegúrate de seleccionar sólo las opciones mostradas en la siguiente imagen:

Escribe ahora el siguiente código en el Editor de Código asociado con el método recién creado:

#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")

    // Obtenemos el peso de la fuente como valor float, tal y como requiere SystemImageWeight

    Var tWeight As CGFloat = SystemImageWeight(weight)

    // Creamos un objeto de configuración para el glifo

    Var symbolConf As ptr = ConfigurationWithPointSize(symbolConfClass, size,tWeight,scale)

    // Obtenemos la NSImagen final para la combinación del glifo con la configuración dada (aún en forma vectorial)
    Var finalImage As ptr = ImageWithSymbolConfiguration(orImage, symbolConf)

    // No es posible crear una imagen para el nombre de glifo recibido, de modo que se devuelve Nil en el caso de que no se haya proporcionad una imagen de respaldo.
    // O bien se colorea la imagen de respaldo en el caso de que se haya proporcionado una.
    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)

    // Aplicamos el coloreado a la imagen en el caso de que hayamos recibido un objeto ColorGroup válido.

    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)

    // Hemos de definir la propiedad Template de la NSImage a False para poder colorearla.
    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)

    // Obtenemos la representación como imagen de mapa de bits para extraer los datos en formato 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)

    // Obtenemos los datos de la imagen para generar el objeto Picture en Xojo.

    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)

    // Necesitamos obtener la cantidad de datos disponibles…
    Var dlen As Integer = DataLength(data)

    // …para crear un MemoryBlock del tamaño correcto.
    Var mb As New MemoryBlock(dlen)
    Var mbPtr As ptr = mb

    // Y ahora podemos volcar los datos PNG del objeto NSData al MemoryBlock.
    GetDataBytes(data,mbPtr,dlen)

    // De modo que obtengamos un Picture a partir de dicho MemoryBlock.
    Return Picture.FromData(mb)
  End If

#EndIf

Como puedes ver, este método llama al método SystemImageWeight, y también hace uso de algunas Estructuras. Añadamos en primer lugar al Módulo el método encargado de convertir el valor de enumeración recibido al valor CGFloat requerido posteriormente por el Declare correspondiente:

  • Method Name: SystemImageWeight
  • Parameters: weight As SystemImageWeights
  • Returned Type: CGFloat
  • Scope: Global

Y escribe el siguiente código en el Editor de Código asociado:

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

Y añadamos ahora tres nuevas Estructuras al módulo macOSLib utilizando los siguientes valores:

  • 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

Añadamos a continuación un método sobrecargado dirigido únicamente a las apps macOS Desktop. La principal diferencia es que este recibirá un parámetro ColorGroup en vez de un tipo de dato 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

Y escribe ahora el siguiente código en el Editor de Código asociado al método:

Var c As Color

If templateColor <> Nil Then
  c = templateColor
End If

Return SystemImage(name,size,weight,scale,c,fallbackTemplateImage)

Haz clic en el icono de la rueda dentada en el Panel Inspector para el método, definiendo los atributos tal y como se muestra en la siguiente imagen:

Asignar los Glifos SF directamente a las Vistas

Añadamos ahora un tercer método al módulo cuya principal diferencia es que no devolverá un Picture. En vez de ello, asignará el glifo SF indicado como imagen sobre el handler del control recibido. Por ejemplo, esto facilita las cosas cuando se quiera asignar un símbolo SF a un botón en la interfaz de usuario (entre otros controles).

Esta es la firma del nuevo método:

  • Name: SystemImage
  • Parameters: name As String, size As Double, weight As SystemImageWeights = SystemImageWeights.Regular, scale As SymbolScale = SymbolScale.small, controlHandler as integer
  • Scope: Global

Y el código que debe escribirse en el Editor de Código asociado:

#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")

    // Obtenemos el peso de la fuente como un valor float requerido por SystemImageWeight.
    Var tWeight As CGFloat = SystemImageWeight(weight)

    // Creamos un objeto de configuración para el glifo.
    Var symbolConf As ptr = ConfigurationWithPointSize(symbolConfClass, size,tWeight,scale)

    // Obtenemos la instancia NSImage final para la combinación de glifo y objeto de configuración (aun en forma vectorial).
    Var finalImage As ptr = ImageWithSymbolConfiguration(orImage, symbolConf)

    // Necesitamos saber si el Handler recibido se corresponde con un objeto que pueda responder al mensaje setImage.

    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:")

    If controlHandler <> 0 And finalImage <> Nil And RespondsToSelector(controlHandler, sel) Then

      Declare Sub Set Lib "AppKit" Selector "setImage:" (control As Integer, Image As ptr)

      // Y asignamos el objeto NSImage al control recibido.
      Set(controlHandler,finalImage)

    End If

  End If

#EndIf

De modo que, por ejemplo, puedes asignar el símbolo Gear (rueda dentada) como la imagen de un control PushButton añadido a la interfaz de usuario de la app usando el siguiente código en el Evento Open de la instancia PushButton:

SystemImage("gearshape.2",14.0, SystemImageWeights.Regular,SymbolScale.Small,Me.Handle)

Resumen

¡Y eso es todo! Como puedes ver, no se incluyen los valores de los glifos directamente en el código, además de que podrás crear un Picture a partir de cualquier símbolo SF con el tamaño, peso y escala deseados. Además, podrás colorearlo y recibir la imagen de respaldo si algo va mal en el proceso.

Puedes descargar el proyecto de ejemplo Xojo con el módulo ya creado y listo para usar desde este enlace.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *