Skip to content

Instantly share code, notes, and snippets.

@LukasCZ
Created February 18, 2022 13:41
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LukasCZ/f78a46e92e0b10767ff0936c5fec716e to your computer and use it in GitHub Desktop.
Save LukasCZ/f78a46e92e0b10767ff0936c5fec716e to your computer and use it in GitHub Desktop.
Helper methods for creating pixel-perfect complications for all Apple Watch sizes.
//
// ComplicationController+helpers.swift
// WatchKit Extension
//
// Created by Lukas Petr on 09.02.2022.
// Distributed under MIT License.
//
//
// Helper methods for creating pixel-perfect complications for all Apple Watch sizes.
//
import Foundation
import WatchKit
import ClockKit
extension ComplicationController {
enum ComplicationImageType {
case graphicCornerTextImage
case graphicCircularStackImage
case graphicCircularImage
case graphicRectangularHeaderImage
case graphicExtraLargeStackImage
case graphicExtraLargeCircularImage
case modularSmallStackImage
case modularSmallSimpleImage
case utilitarianSmallFlatImage
case utilitarianSmallSquare
case circularSmallStackImage
case circularSmallSimpleImage
case extraLargeStackImage
}
struct ComplicationImageSizeCollection {
var size38mm: CGFloat = 0
let size40mm: CGFloat
let size41mm: CGFloat
let size44mm: CGFloat
let size45mm: CGFloat
var thickness: ComplicationImageLineThickness = .normal
// The following sizes are taken directly from HIG: https://developer.apple.com/design/human-interface-guidelines/watchos/overview/complications/
static let graphicCornerTextImageSizes = ComplicationImageSizeCollection(size40mm: 20, size41mm: 21, size44mm: 22, size45mm: 24, thickness: .normal)
static let graphicCircularStackImageSizes = ComplicationImageSizeCollection(size40mm: 14, size41mm: 15, size44mm: 16, size45mm: 16.5, thickness: .thicker)
static let graphicCircularImageSizes = ComplicationImageSizeCollection(size40mm: 42, size41mm: 44.5, size44mm: 47, size45mm: 50, thickness: .normal)
static let graphicRectangularHeaderImageSizes = ComplicationImageSizeCollection(size40mm: 12, size41mm: 12.5, size44mm: 13.5, size45mm: 14.5, thickness: .thicker)
static let graphicExtraLargeStackImageSizes = ComplicationImageSizeCollection(size40mm: 40, size41mm: 42, size44mm: 44, size45mm: 47 /* this one is wrong in HIG */, thickness: .thicker)
static let graphicExtraLargeCircularImageSizes = ComplicationImageSizeCollection(size40mm: 120, size41mm: 126, size44mm: 132, size45mm: 143)
static let modularSmallStackImageSizes = ComplicationImageSizeCollection(size38mm: 14, size40mm: 15, size41mm: 16, size44mm: 17, size45mm: 18, thickness: .thicker)
static let modularSmallSimpleImageSizes = ComplicationImageSizeCollection(size38mm: 26, size40mm: 29, size41mm: 30.5, size44mm: 32, size45mm: 34.5, thickness: .normal)
static let utilitarianSmallFlatImageSizes = ComplicationImageSizeCollection(size38mm: 9, size40mm: 10, size41mm: 10.5, size44mm: 11, size45mm: 12, thickness: .thicker)
static let utilitarianSmallSquareSizes = ComplicationImageSizeCollection(size38mm: 20, size40mm: 22, size41mm: 23.5, size44mm: 25, size45mm: 26, thickness: .normal)
static let circularSmallStackImageSizes = ComplicationImageSizeCollection(size38mm: 7, size40mm: 8, size41mm: 8.5, size44mm: 9, size45mm: 9.5, thickness: .thicker)
static let circularSmallSimpleImageSizes = ComplicationImageSizeCollection(size38mm: 16, size40mm: 18, size41mm: 19, size44mm: 20, size45mm: 21.5, thickness: .normal)
static let extraLargeStackImageSizes = ComplicationImageSizeCollection(size38mm: 42, size40mm: 45, size41mm: 47.5, size44mm: 51, size45mm: 53.5, thickness: .normal)
func sizeForCurrentWatchModel() -> CGFloat {
let screenHeight = WKInterfaceDevice.current().screenBounds.size.height
if screenHeight >= 242 {
// It's the 45mm version..
return self.size45mm
}
else if screenHeight >= 224 {
// It's the 44mm version..
return self.size44mm
}
else if screenHeight >= 215 {
// It's the 41mm version..
return self.size41mm
}
else if screenHeight >= 197 {
return self.size40mm
}
else if screenHeight >= 170 {
return self.size38mm
}
return self.size40mm // Fallback, just in case.
}
static func sizes(for type: ComplicationImageType) -> ComplicationImageSizeCollection {
switch type {
case .graphicCornerTextImage: return Self.graphicCornerTextImageSizes
case .graphicCircularStackImage: return Self.graphicCircularStackImageSizes
case .graphicCircularImage: return Self.graphicCircularImageSizes
case .graphicRectangularHeaderImage: return Self.graphicRectangularHeaderImageSizes
case .graphicExtraLargeStackImage: return Self.graphicExtraLargeStackImageSizes
case .graphicExtraLargeCircularImage: return Self.graphicExtraLargeCircularImageSizes
case .modularSmallStackImage: return Self.modularSmallStackImageSizes
case .modularSmallSimpleImage: return Self.modularSmallSimpleImageSizes
case .utilitarianSmallFlatImage: return Self.utilitarianSmallFlatImageSizes
case .utilitarianSmallSquare: return Self.utilitarianSmallSquareSizes
case .circularSmallStackImage: return Self.circularSmallStackImageSizes
case .circularSmallSimpleImage: return Self.circularSmallSimpleImageSizes
case .extraLargeStackImage: return Self.extraLargeStackImageSizes
}
}
}
//
// Here follow some examples of methods which can be used to create concrete CLKImageProviders.
// The general idea is that based on the passed-in ComplicationImageType, the size of the image is obtained,
// and then that gets used when drawing an image using Core Graphics.
//
// MARK: - Colored timer images
func coloredTimerImageProvider(for type: ComplicationImageType, color: UIColor, rounded: Bool = false) -> CLKFullColorImageProvider {
let complicationImageSizes = ComplicationImageSizeCollection.sizes(for: type)
let width = complicationImageSizes.sizeForCurrentWatchModel()
let size = CGSize(width: width, height: width)
// First draw the background
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
let context = UIGraphicsGetCurrentContext()!
// TODO: Draw your symbol here.
context.setFillColor(color.cgColor)
context.fill(rect)
var fullColorImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
if rounded == true {
fullColorImage = fullColorImage.roundedImage
}
// TODO: Draw this one using Core Graphics too. It will be used for the "tinted" version.
let timerSymbolImage: UIImage
let tintedImageProvider = CLKImageProvider(onePieceImage: timerSymbolImage)
return CLKFullColorImageProvider(fullColorImage: fullColorImage, tintedImageProvider: tintedImageProvider)
}
// MARK: - Two-piece images
func timerImageProvider(for type: ComplicationImageType, withTwoPieceImages: Bool = false) -> CLKImageProvider {
let complicationImageSizes = ComplicationImageSizeCollection.sizes(for: type)
let width = complicationImageSizes.sizeForCurrentWatchModel()
let size = CGSize(width: width, height: width)
// First, create the timer symbol image (one-piece) (using the thicker version for now..)
// TODO: Draw this one using Core Graphics
if withTwoPieceImages == true {
// Create the background (circle)
// TODO: Draw this one using Core Graphics
let backgroundCircleImage : UIImage
// Create the foreground (clock hands)
// TODO: Draw this one using Core Graphics
let foregroundClockHandsImage : UIImage
return CLKImageProvider(onePieceImage: timerSymbolImage,
twoPieceImageBackground: backgroundCircleImage,
twoPieceImageForeground: foregroundClockHandsImage)
}
return CLKImageProvider(onePieceImage: timerSymbolImage)
}
// MARK: - Timelines icon images
func timelinesIconImageProvider(for type: ComplicationImageType) -> CLKFullColorImageProvider {
let complicationImageSizes = ComplicationImageSizeCollection.sizes(for: type)
let width = complicationImageSizes.sizeForCurrentWatchModel()
let size = CGSize(width: width, height: width)
// TODO: Draw this one using Core Graphics
let tintedImage : UIImage
let tintedImageProvider = CLKImageProvider(onePieceImage: tintedImage)
let timelinesIconImage = self.renderPDFToImage(named: "Timelines-watch-icon.pdf", outputSize: size)
return CLKFullColorImageProvider(fullColorImage: timelinesIconImage, tintedImageProvider: tintedImageProvider)
}
func renderPDFToImage(named filename: String, outputSize size: CGSize) -> UIImage {
// Create a URL for the PDF file
let resourceName = filename.replacingOccurrences(of: ".pdf", with: "")
let path = Bundle.main.path(forResource: resourceName, ofType: "pdf")!
let url = URL(fileURLWithPath: path)
guard let document = CGPDFDocument(url as CFURL),
let page = document.page(at: 1) else {
fatalError("We couldn't find the document or the page")
}
let originalPageRect = page.getBoxRect(.mediaBox)
// With the multiplier, we bring the pdf from its original size to the desired output size.
let multiplier = size.width / originalPageRect.width
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let context = UIGraphicsGetCurrentContext()!
// Translate the context
context.translateBy(x: 0, y: (originalPageRect.size.height * multiplier))
// Flip the context vertically because the Core Graphics coordinate system starts from the bottom.
context.scaleBy(x: multiplier * 1.0, y: -1.0 * multiplier)
// Draw the PDF page
context.drawPDFPage(page)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
extension UIImage {
var roundedImage: UIImage {
let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
UIBezierPath(roundedRect: rect, cornerRadius: self.size.width / 2).addClip()
self.draw(in: rect)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return finalImage
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment