Created
February 18, 2022 13:41
-
-
Save LukasCZ/f78a46e92e0b10767ff0936c5fec716e to your computer and use it in GitHub Desktop.
Helper methods for creating pixel-perfect complications for all Apple Watch sizes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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