{"version":"https://jsonfeed.org/version/1","title":"Ramon Torres","home_page_url":"https://rtorres.me","feed_url":"https://rtorres.me/blog.json","favicon":"https://rtorres.me/favicon.png","items":[{"id":"https://rtorres.me/blog/codable-uicolor-via-property-wrapper/","url":"https://rtorres.me/blog/codable-uicolor-via-property-wrapper/","title":"Codable UIColor via property wrapper","content_html":"
We can add Codable
support to UIColor
with the help of a property wrapper. The following property wrapper allows us to encode and decode UIColor
values as hex strings:
import UIKit\nimport ColorToolbox\n\n@propertyWrapper\nstruct HexCodableColor: Codable {\n\n var wrappedValue: UIColor\n\n init(wrappedValue: UIColor) {\n self.wrappedValue = wrappedValue\n }\n\n init(from decoder: Decoder) throws {\n let container = try decoder.singleValueContainer()\n let hexString = try container.decode(String.self)\n\n guard let color = UIColor(hex: hexString) else {\n throw DecodingError.dataCorruptedError(\n in: container,\n debugDescription: \"The value is not a valid hex color\"\n )\n }\n\n wrappedValue = color\n }\n\n func encode(to encoder: Encoder) throws {\n var container = encoder.singleValueContainer()\n try container.encode(wrappedValue.toHex())\n }\n\n}\n
\nThe property wrapper uses the init?(hex:)
and toHex()
extensions from ColorToolbox.
Apply the wrapper to a UIColor
property in a Codable
struct/class:
struct MyModel: Codable {\n // ...\n @HexCodableColor var color: UIColor\n}\n
\nNow we can encode/decode the struct the same way we would with any other Codable
type:
let model = MyModel(color: .red)\n\nlet encoder = JSONEncoder()\nlet data = try encoder.encode(model) // -> {\"color\":\"#FF0000\"}\n
\n","date_published":"2023-06-08T02:37:00Z"},{"id":"https://rtorres.me/blog/how-to-convert-uicolor-to-hex-in-swift/","url":"https://rtorres.me/blog/how-to-convert-uicolor-to-hex-in-swift/","title":"How to convert UIColor to hex in Swift","content_html":"Here's a simple extension to convert a UIColor
to a hex string in Swift:
extension UIColor {\n\n func toHex() -> String {\n var red: CGFloat = 0\n var green: CGFloat = 0\n var blue: CGFloat = 0\n var alpha: CGFloat = 0\n\n guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {\n assertionFailure(\"Failed to get RGBA components from UIColor\")\n return \"#000000\"\n }\n\n // Clamp components to [0.0, 1.0]\n red = max(0, min(1, red))\n green = max(0, min(1, green))\n blue = max(0, min(1, blue))\n alpha = max(0, min(1, alpha))\n\n if alpha == 1 {\n // RGB\n return String(\n format: \"#%02lX%02lX%02lX\",\n Int(round(red * 255)),\n Int(round(green * 255)),\n Int(round(blue * 255))\n )\n } else {\n // RGBA\n return String(\n format: \"#%02lX%02lX%02lX%02lX\",\n Int(round(red * 255)),\n Int(round(green * 255)),\n Int(round(blue * 255)),\n Int(round(alpha * 255))\n )\n }\n }\n\n}\n
\nDisplay P3 colors are truncated to fall within the sRGB gamut.
\nlet color = UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1)\nprint(color.toHex()) // #808080\n
\nHere is a simple extension to UIColor
for creating colors from hex strings. It can parse hex colors in #RRGGBB
and #RRGGBBAA
formats, as well as the #RGB
shorthand:
import UIKit\n\nextension UIColor {\n\n convenience init?(hex: String) {\n let scanner = Scanner(string: hex)\n scanner.charactersToBeSkipped = nil\n\n // Consume optional `#`\n _ = scanner.scanString(\"#\")\n\n switch scanner.charactersLeft() {\n case 6, 8:\n guard let red = scanner.scanHexByte(),\n let green = scanner.scanHexByte(),\n let blue = scanner.scanHexByte() else {\n return nil\n }\n\n var alpha: UInt8 = 255\n\n // Parse alpha if available\n if scanner.charactersLeft() == 2 {\n guard let parsedAlpha = scanner.scanHexByte() else {\n return nil\n }\n\n alpha = parsedAlpha\n }\n\n self.init(\n red: CGFloat(red) / 255,\n green: CGFloat(green) / 255,\n blue: CGFloat(blue) / 255,\n alpha: CGFloat(alpha) / 255\n )\n case 3:\n guard let red = scanner.scanHexNibble(),\n let green = scanner.scanHexNibble(),\n let blue = scanner.scanHexNibble() else {\n return nil\n }\n\n self.init(\n red: CGFloat(red) / 15,\n green: CGFloat(green) / 15,\n blue: CGFloat(blue) / 15,\n alpha: 1\n )\n default:\n return nil\n }\n }\n\n}\n\nprivate extension Scanner {\n\n func scanHexNibble() -> UInt8? {\n guard let character = scanCharacter(), character.isHexDigit else {\n return nil\n }\n\n return UInt8(String(character), radix: 16)\n }\n\n func scanHexByte() -> UInt8? {\n guard let highNibble = scanHexNibble(), let lowNibble = scanHexNibble() else {\n return nil\n }\n\n return (highNibble << 4) | lowNibble\n }\n\n func charactersLeft() -> Int {\n return string.count - currentIndex.utf16Offset(in: string)\n }\n\n}\n
\nTo use it, simply call the new initializer with a hex string:
\nlet color = UIColor(hex: \"#9082F8\")\n
\nNSUserDefaults is one of the core classes of OpenStep that have been there since its initial specification.
\nNSUserDefaults was designed and implemented around NSMutableDictionary. Because of this, C scalar types such as int, float, bool, and double, have to be boxed/unboxed behind the scenes. As NSDictionary can only hold objects as values.
\nCurrently, if we want to store an Enum value in UserDefaults, the easiest way is to make the Enum conform to RawRepresentable and save the raw value of the member we want to store.
\nenum DistanceUnit: String {\n case automatic\n case metric\n case imperial\n}\n
\nclass Preferences {\n\n let userDefaults: UserDefaults = .standard\n\n var distanceUnit: DistanceUnit {\n set {\n userDefaults.set(newValue.rawValue, forKey: \"distance-unit\")\n }\n get {\n guard let rawValue = userDefaults.string(forKey: \"distance-unit\") else {\n return .automatic\n }\n\n return DistanceUnit(rawValue: rawValue) ?? .automatic\n }\n }\n\n}\n
\nAs you can see, this can get a little bit too verbose the more Enum-backed preferences you store.
\nLuckily, Swift has support for extensions and generics. We can leverage this to extend UserDefaults and add support for storing and retrieving values that conform to RawRepresentable
.
// UserDefaults+RawRepresentable.swift\nimport Foundation\n\nextension UserDefaults {\n\n func value<T: RawRepresentable>(_ type: T.Type, forKey key: String) -> T? {\n guard let value = object(forKey: key) as? T.RawValue else {\n return nil\n }\n\n return T(rawValue: value)\n }\n\n func value<T: RawRepresentable>(_ type: T.Type, forKey key: String, default: T) -> T {\n return value(type, forKey: key) ?? `default`\n }\n\n func set<T: RawRepresentable>(_ value: T, forKey key: String) {\n set(value.rawValue, forKey: key)\n }\n\n}\n
\nclass Preferences {\n\n let userDefaults: UserDefaults = .standard\n\n var distanceUnit: DistanceUnit {\n set {\n userDefaults.set(newValue, forKey: \"distance-unit\")\n }\n get {\n userDefaults.value(DistanceUnit.self, forKey: \"distance-unit\", default: .automatic)\n }\n }\n\n}\n
\n","date_published":"2021-05-30T22:24:00Z"},{"id":"https://rtorres.me/blog/easy-reusable-cells-with-swift-protocols-and-generics/","url":"https://rtorres.me/blog/easy-reusable-cells-with-swift-protocols-and-generics/","title":"Easy Reusable Cells with Swift Protocols and Generics","content_html":"One of the first things that most iOS developers learn is how to present data in table or collection views, which involves registering and dequeuing reusable cells using string identifiers. The typical code that I see around usually looks like this:
\nclass MoviesController: UITableViewController {\n\n // ...\n\n override func viewDidLoad() {\n super.viewDidLoad()\n tableView.register(MovieCell.self, forCellReuseIdentifier: \"MovieCell\")\n }\n\n // ...\n\n override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n let cell = tableView.dequeueReusableCell(withIdentifier: \"MovieCell\", for: indexPath) as! MovieCell\n\n // ...\n\n return cell\n }\n\n}\n
\nA safer version of this code replaces the identifier strings with a constant and the forced typecast with a guarded precondition.
\nclass MoviesController: UITableViewController {\n\n static let cellIdentifier = \"MovieCell\"\n\n // ...\n\n override func viewDidLoad() {\n super.viewDidLoad()\n tableView.register(MovieCell.self, forCellReuseIdentifier: Self.cellIdentifier)\n }\n\n // ...\n\n override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n guard let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellIdentifier, for: indexPath) as? MovieCell else {\n preconditionFailure(\"The returned cell is not a MovieCell\")\n }\n\n // ...\n\n return cell\n }\n\n}\n
\nThis approach works well for table or collection views of a single cell type. But it gets a bit overwhelming if you need to support multiple cell types in a single collection.
\nA pattern that I have been using for quite a while is something that I call Easy Reusable Cells. This technique allows to register one or more cells without explicitly providing cell identifiers as follows:
\noverride func viewDidLoad() {\n super.viewDidLoad()\n\n tableView.registerCells([\n AdCell.self,\n MovieCell.self,\n FeaturedMovieCell.self,\n ])\n}\n
\nBut it also allows for reusable cell dequeuing in a strongly typed way:
\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n // ...\n\n let cell = tableView.dequeueCell(MovieCell.self, for: indexPath)\n cell.configure(movie)\n return cell\n\n // ...\n}\n
\nThe heavy lifting is done behind the scenes by a protocol and a couple of extensions, all contained in a single file that I always add to my projects:
\n//\n// ReusableCell.swift\n//\n\nimport UIKit\n\n/// A protocol that all reusable cells must implement.\nprotocol ReusableCell: AnyObject {\n static var cellIdentifier: String { get }\n}\n\n// Default protocol implementation\nextension ReusableCell {\n\n static var cellIdentifier: String {\n return String(describing: self)\n }\n\n}\n\n// MARK: - Table View\n\nextension UITableViewCell: ReusableCell {}\n\nextension UITableView {\n\n /// Registers a set of classes for use in creating new table view cells.\n /// - Parameter cellTypes: The cell types to register\n func registerCells(_ cellTypes: [ReusableCell.Type]) {\n for cellType in cellTypes {\n register(cellType, forCellReuseIdentifier: cellType.cellIdentifier)\n }\n }\n\n /// Dequeues a reusable table cell object located by its type.\n /// - Parameters:\n /// - type: The type of the cell to dequeue.\n /// - indexPath: The index path specifying the location of the cell.\n /// - Returns: A valid cell instance of the specified type.\n func dequeueCell<T: ReusableCell>(_ type: T.Type, for indexPath: IndexPath) -> T {\n guard let cell = dequeueReusableCell(\n withIdentifier: type.cellIdentifier,\n for: indexPath\n ) as? T else {\n preconditionFailure(\"Unexpected cell type returned\")\n }\n\n return cell\n }\n\n}\n\n// MARK: - Collection View\n\nextension UICollectionViewCell: ReusableCell {}\n\nextension UICollectionView {\n\n /// Registers a set of classes for use in creating new collection view cells.\n /// - Parameter cellTypes: The cell types to register\n func registerCells(_ cellTypes: [ReusableCell.Type]) {\n for cellType in cellTypes {\n register(cellType, forCellWithReuseIdentifier: cellType.cellIdentifier)\n }\n }\n\n /// Dequeues a reusable cell object located by its type.\n /// - Parameters:\n /// - type: The type of the cell to dequeue.\n /// - indexPath: The index path specifying the location of the cell.\n /// - Returns: A valid cell instance of the specified type.\n func dequeueCell<T: ReusableCell>(_ type: T.Type, for indexPath: IndexPath) -> T {\n guard let cell = dequeueReusableCell(\n withReuseIdentifier: type.cellIdentifier,\n for: indexPath\n ) as? T else {\n preconditionFailure(\"Unexpected cell type returned\")\n }\n\n return cell\n }\n\n}\n
\nYou can easily adapt this code to work with Nibs, if needed, by adding a few lines of code. 🪄
\n","date_published":"2021-03-27T00:14:00Z"},{"id":"https://rtorres.me/blog/getting-emoji-to-work-in-asciidoctor-pdf/","url":"https://rtorres.me/blog/getting-emoji-to-work-in-asciidoctor-pdf/","title":"Getting Emoji to Work in Asciidoctor PDF","content_html":"At Gooroo, we use AsciiDoc + Asciidoctor for publishing our Tutor Handbook. This setup allows us to generate HTML, ePub, and PDF versions of the handbook from a single source file.
\nOne issue we ran into is the lack of out-of-the-box support for emojis in the Asciidoctor PDF converter, which appears to be caused by some limitations in the TTF library used by Prawn, the PDF writer used by Asciidoctor PDF.
\nA simple workaround that I found is to use Prawn::Emoji, a gem that adds emoji support to Prawn. For it to work, assuming you are using Rake, you need to require Prawn::Emoji before requiring Asciidoctor as follows:
\n# Rakefile\nrequire 'prawn/emoji' # Must be required before asciidoctor and asciidoctor-pdf\nrequire 'asciidoctor'\nrequire 'asciidoctor-pdf'\nrequire 'asciidoctor-epub3'\n\ndesc 'Generate the PDF version of the handbook'\ntask :pdf do\n Asciidoctor.convert_file 'src/epub.adoc',\n safe: :unsafe,\n backend: 'pdf',\n to_dir: 'dist',\n mkdirs: true,\n to_file: 'handbook.pdf',\n attributes: [\n 'imagesdir=images',\n 'pdf-theme=./theme.yml',\n 'pdf-fontsdir=./src/fonts'\n ]\nend\n\n# ...\n
\n✨
\n","date_published":"2020-09-23T23:14:00Z"},{"id":"https://rtorres.me/blog/lerp-swift/","url":"https://rtorres.me/blog/lerp-swift/","title":"Lerp.swift - Linear Interpolation in Swift","content_html":"Below are some lerp\nfunctions that I implemented while working on a side project.
\nThe first function is templated to work with any type that conforms to\nBinaryFloatingPoint (Float, Double, CGFloat, TimeInterval, etc.).\nThe other three functions operate on Core Graphics structs\n(CGPoint, CGSize, and CGRect).
\nUpdate (2023-01-24): Added inverseLerp
function and documentation.
//\n// Lerp.swift\n//\n// Written by Ramon Torres\n// Placed under public domain.\n//\n\nimport CoreGraphics\n\n// swiftlint:disable identifier_name\n\n/// Linearly interpolates between two values.\n///\n/// Interpolates between the values `v0` and `v1` by a factor `t`.\n///\n/// - Parameters:\n/// - v0: The first value.\n/// - v1: The second value.\n/// - t: The interpolation factor. Between `0` and `1`.\n/// - Returns: The interpolated value.\n@inline(__always)\nfunc lerp<V: BinaryFloatingPoint, T: BinaryFloatingPoint>(_ v0: V, _ v1: V, _ t: T) -> V {\n return v0 + V(t) * (v1 - v0)\n}\n\n/// Linearly interpolates between two points.\n///\n/// Interpolates between the points `p0` and `p1` by a factor `t`.\n///\n/// - Parameters:\n/// - p0: The first point.\n/// - p1: The second point.\n/// - t: The interpolation factor. Between `0` and `1`.\n/// - Returns: The interpolated point.\n@inline(__always)\nfunc lerp<T: BinaryFloatingPoint>(_ p0: CGPoint, _ p1: CGPoint, _ t: T) -> CGPoint {\n return CGPoint(\n x: lerp(p0.x, p1.x, t),\n y: lerp(p0.y, p1.y, t)\n )\n}\n\n/// Linearly interpolates between two sizes.\n///\n/// Interpolates between the sizes `s0` and `s1` by a factor `t`.\n///\n/// - Parameters:\n/// - s0: The first size.\n/// - s1: The second size.\n/// - t: The interpolation factor. Between `0` and `1`.\n/// - Returns: The interpolated size.\n@inline(__always)\nfunc lerp<T: BinaryFloatingPoint>(_ s0: CGSize, _ s1: CGSize, _ t: T) -> CGSize {\n return CGSize(\n width: lerp(s0.width, s1.width, t),\n height: lerp(s0.height, s1.height, t)\n )\n}\n\n/// Linearly interpolates between two rectangles.\n///\n/// Interpolates between the rectangles `r0` and `r1` by a factor `t`.\n///\n/// - Parameters:\n/// - r0: The first rectangle.\n/// - r1: The second rectangle.\n/// - t: The interpolation factor. Between `0` and `1`.\n/// - Returns: The interpolated rectangle.\n@inline(__always)\nfunc lerp<T: BinaryFloatingPoint>(_ r0: CGRect, _ r1: CGRect, _ t: T) -> CGRect {\n return CGRect(\n origin: lerp(r0.origin, r1.origin, t),\n size: lerp(r0.size, r1.size, t)\n )\n}\n\n/// Inverse linear interpolation.\n///\n/// Given a value `v` between `v0` and `v1`, returns the interpolation factor `t`\n/// such that `v == lerp(v0, v1, t)`.\n///\n/// - Parameters:\n/// - v0: The lower bound of the interpolation range.\n/// - v1: The upper bound of the interpolation range.\n/// - v: The value to interpolate.\n/// - Returns: The interpolation factor `t` such that `v == lerp(v0, v1, t)`.\n@inline(__always)\nfunc inverseLerp<V: BinaryFloatingPoint, T: BinaryFloatingPoint>(_ v0: V, _ v1: V, _ v: V) -> T {\n return T((v - v0) / (v1 - v0))\n}\n\n// swiftlint:enable identifier_name\n
\nAfter adding the above code to your project, you can use the lerp
function as follows:
let numberA = 0.0\nlet numberB = 240.0\n\nlet t = 0.5 // 50% of the way between numberA and numberB\n\nlet result = lerp(numberA, numberB, t) // -> 120.0\n
\n","date_published":"2020-07-02T17:27:00Z"},{"id":"https://rtorres.me/blog/code-snippet/validating-routing-numbers-in-python/","url":"https://rtorres.me/blog/code-snippet/validating-routing-numbers-in-python/","title":"Code Snippet: Validate Routing Numbers in Python","content_html":"The following code snippet can be used to check if an ABA routing number is potentially valid.
\ndef valid_routing_number(routing_number: str) -> bool:\n if len(routing_number) != 9:\n # Routing numbers are always 9 digits\n return False\n\n if not routing_number.isnumeric():\n # Routing numbers are always numeric\n return False\n\n # Calculate checksum\n checksum = (3 * (int(routing_number[0]) + int(routing_number[3]) + int(routing_number[6]))) + \\\n (7 * (int(routing_number[1]) + int(routing_number[4]) + int(routing_number[7]))) + \\\n (1 * (int(routing_number[2]) + int(routing_number[5]) + int(routing_number[8])))\n\n # If the checksum is a multiple of 10, the number is potentially valid\n return (checksum % 10) == 0\n
\nNote: The function doesn't check if the routing number is actually assigned to a bank. You can delegate the extra validation to your payment processor or maintain a list of valid routing numbers on your end.
\n","date_published":"2019-02-03T18:45:15Z"},{"id":"https://rtorres.me/blog/code-snippet/power-rangers-communicator-sound-in-arduino/","url":"https://rtorres.me/blog/code-snippet/power-rangers-communicator-sound-in-arduino/","title":"Code Snippet: Power Rangers Communicator Sound in Arduino","content_html":"#define BUZZER_PIN 8\n\n#define SILENCE 0\n#define NOTE_B7 3951\n#define NOTE_CS8 4435\n#define NOTE_E8 5274\n\nstatic const int notes[] = {\n NOTE_CS8, SILENCE, NOTE_CS8, SILENCE,\n NOTE_B7, NOTE_CS8, SILENCE, NOTE_E8,\n SILENCE, NOTE_CS8, SILENCE, SILENCE\n};\n\nvoid playCommunicatorSound() {\n for (int i=0; i<12; i+=1) {\n if (notes[i] != SILENCE) {\n tone(BUZZER_PIN, notes[i], 102);\n }\n\n delay(150);\n }\n}\n
\n","date_published":"2017-05-04T22:08:34Z"},{"id":"https://rtorres.me/blog/detecting-when-other-apps-play-audio-on-ios/","url":"https://rtorres.me/blog/detecting-when-other-apps-play-audio-on-ios/","title":"Detecting when other apps play audio on iOS","content_html":"Apps that play ambient audio, such as games, should not interrupt other apps that play audio in the background. We can accomplish this by setting the appropriate category on the shared AVAudioSession
instance:
let audioSession = AVAudioSession.sharedInstance()\ntry audioSession.setCategory(.ambient, mode: .default)\n
\nBy setting the category to .ambient
, we tell iOS that mixing the audio from your app with other apps is acceptable.
To avoid clashing, apps like games may also want to limit playback to sound effects only and mute their soundtrack while music apps play in the background. We can easily do this by checking the AVAudioSession.secondaryAudioShouldBeSilencedHint
property:
let audioSession = AVAudioSession.sharedInstance()\nif audioSession.secondaryAudioShouldBeSilencedHint {\n // Mute the game's background music.\n} else {\n // Play game's background music!\n}\n
\nYou can detect when other apps start and stop playing audio by subscribing to AVAudioSession.silenceSecondaryAudioHintNotification
.
NotificationCenter.default.addObserver(\n self,\n selector: #selector(handleSecondaryAudioHintChange(_:)),\n name: AVAudioSession.silenceSecondaryAudioHintNotification,\n object: nil\n)\n\n// ...\n\n@objc\nfunc handleSecondaryAudioHintChange(_ notification: Notification) {\n guard let typeValue = notification.userInfo?[AVAudioSessionSilenceSecondaryAudioHintTypeKey] as? UInt,\n let hintType = AVAudioSession.SilenceSecondaryAudioHintType(rawValue: typeValue) else {\n return\n }\n\n if hintType == .begin {\n // Other app started playing audio... Mute game's background music.\n } else {\n // Other app stopped playing audio... Resume playing background music.\n }\n}\n
\nI recommend creating an “Audio Manager” singleton class to glue everything together. The can be responsible for playing the looping audio files that make the soundtrack, registering itself for secondary audio hint notification changes, and starting or stopping the music accordingly.
\n","date_published":"2014-12-22T13:58:21Z"},{"id":"https://rtorres.me/blog/five-tips-for-localizing-ios-apps/","url":"https://rtorres.me/blog/five-tips-for-localizing-ios-apps/","title":"5 Tips for Localizing iOS Apps","content_html":"Below are five tips for localizing iOS apps based on my own experience as a project lead and product designer:
\nLong gone are the days of sending Excel sheets packed with localization strings back and forth via email. During the last few years, there has been a proliferation of web-based tools for managing localization. These tools allow you to track localization strings across multiple projects, collaborate with translators, even hire them right from within the platform.
\nSome of the platforms that I have used or evaluated are:
\nOther platforms that I haven't used yet, but I would love to try are Shuttle (Open source) and OneSky.
\nWhile localizing apps, you will find yourself doing many repetitive tasks such as exporting and importing strings, scanning your code for new strings, etc. The only way to keep this part of the process sane is by automating it as much as possible. Luckily, some of the platforms I listed above already have APIs, even command-line clients, to help you integrate them into your development pipeline.
\nThe translators are going to need as much context information as possible. Start by writing down what your app does and what is the use case is of each feature. By doing this, you will give them the context they need to get started.
\nTake screenshots of every screen that needs to be localized. And if your app contains instructions such as \"Go to Settings and enable Location Services,\" make sure you take screenshots of these steps in all your target languages. You want your translators to use the exact text that Apple uses in their interface to avoid confusing the end-user.
\n<figure>\n<img src=\"/images/posts/settings-spanish.png\" alt=\"Difference between Spanish and Mexican Spanish as seen in the Settings app.\" />\n<figcaption>Difference between Spanish and Mexican Spanish as seen in the Settings app.</figcaption>\n</figure>
\nIf you speak multiple languages, try using your app in your other languages. Sometimes I change my iPhone language settings to Spanish and use it like that for an entire day. I find this practice very helpful for refining localizations and spotting out localization and layout issues.
\nLocalizing is a long-term commitment. Once you localize, it is hard to roll back localization without upsetting users. As you add new features, you must localize those features before shipping them. When estimating, always account for localization work and testing.
\nHappy localizing! 🌐
\n","date_published":"2014-11-21T22:08:34Z"},{"id":"https://rtorres.me/blog/images-cant-contain-alpha-channels-or-transparencies/","url":"https://rtorres.me/blog/images-cant-contain-alpha-channels-or-transparencies/","title":"Images can’t contain alpha channels or transparencies.","content_html":"While updating the screenshots of one of our apps via the new iTunes Connect interface I was getting the following error:
\n\n\nImages can’t contain alpha channels or transparencies.
\n
I was able to strip out the alpha channels off the App Store assets using ImageMagick's mogrify command.
\n$ mogrify -alpha off -format png screenshots/*.png\n
\nThe mogrify
command is an in-place batch processing utility. Extra care has to be taken because by default it will overwrite your images.
CGFloat IKBHairlineThickness() {\n CGFloat scale = [[UIScreen mainScreen] scale];\n return (1.0 / scale);\n}\n
\nWhat is a hairline anyways?
\nHairlines are 1 pixel thick separators used everywhere in iOS. From the top\nedges of UITabBars to UITableView
separators.