Last active
October 24, 2023 01:35
-
-
Save natmark/ef27845aff19059e74916df421223b79 to your computer and use it in GitHub Desktop.
https://techlife.cookpad.com/entry/2023/09/14/160000 で紹介しているUI要素のフレーム描画を行うデバッグ用のView実装です
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
// This project is licensed under the MIT No Attribution license. | |
// | |
// Copyright (c) 2023 Cookpad Inc. | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
import UIKit | |
final class DebugFrameInspectorView: UIView { | |
private lazy var numberFormatter: NumberFormatter = { | |
let formatter = NumberFormatter() | |
formatter.numberStyle = .decimal | |
formatter.groupingSize = 3 | |
formatter.groupingSeparator = "," | |
formatter.maximumFractionDigits = 2 | |
formatter.roundingMode = .halfUp | |
return formatter | |
}() | |
private struct AccentColor { | |
static var red: CGColor = UIColor.red.cgColor | |
static var blue: CGColor = UIColor(red: 88 / 255.0, green: 185 / 255.0, blue: 248 / 255.0, alpha: 1.0).cgColor | |
static var purple: CGColor = UIColor(red: 152 / 255.0, green: 93 / 255.0, blue: 246 / 255.0, alpha: 1.0).cgColor | |
static var green: CGColor = UIColor(red: 95 / 255.0, green: 204 / 255.0, blue: 137 / 255.0, alpha: 1.0).cgColor | |
} | |
private struct ViewWireframe { | |
var rect: CGRect | |
var cornerRadius: CGFloat | |
var maskedCorners: CACornerMask | |
} | |
private struct SinglePressValue { | |
var viewWireframe: ViewWireframe | |
} | |
private struct DoublePressValue { | |
var baseViewWireframe: ViewWireframe | |
var targetViewWireframe: ViewWireframe | |
} | |
private var singlePressValue: SinglePressValue? = nil | |
private var doublePressValue: DoublePressValue? = nil | |
init() { | |
super.init(frame: .zero) | |
backgroundColor = .clear | |
isUserInteractionEnabled = false | |
} | |
@available(*, unavailable) | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func draw(_ rect: CGRect) { | |
super.draw(rect) | |
if let singlePressValue { | |
drawViewWireframe( | |
singlePressValue.viewWireframe, | |
shouldShowScreenMargin: true, | |
shouldShowCorner: true | |
) | |
} | |
if let doublePressValue { | |
drawViewWireframe( | |
doublePressValue.baseViewWireframe, | |
shouldShowScreenMargin: false, | |
shouldShowCorner: false | |
) | |
drawViewWireframe( | |
doublePressValue.targetViewWireframe, | |
shouldShowScreenMargin: false, | |
shouldShowCorner: false | |
) | |
drawComponentMarginLine( | |
baseRect: doublePressValue.baseViewWireframe.rect, | |
targetRect: doublePressValue.targetViewWireframe.rect | |
) | |
} | |
} | |
func setup() { | |
let singleLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didSingleLongPress(_:))) | |
singleLongPressGestureRecognizer.minimumPressDuration = 0.2 | |
singleLongPressGestureRecognizer.numberOfTouchesRequired = 1 | |
window?.addGestureRecognizer(singleLongPressGestureRecognizer) | |
let doubleLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(didDoubleLongPress(_:))) | |
doubleLongPressGestureRecognizer.minimumPressDuration = 0.2 | |
doubleLongPressGestureRecognizer.numberOfTouchesRequired = 2 | |
window?.addGestureRecognizer(doubleLongPressGestureRecognizer) | |
} | |
/// 1点で長押しした際に呼ばれる関数 | |
@objc private func didSingleLongPress(_ sender: UILongPressGestureRecognizer) { | |
if sender.state == .began { | |
let positionInWindow = sender.location(in: window) | |
if let hitView = window?.hitTest(positionInWindow, with: nil) { | |
let positionInHitView = sender.location(in: hitView) | |
let globalRect = CGRect( | |
x: positionInWindow.x - positionInHitView.x, | |
y: positionInWindow.y - positionInHitView.y, | |
width: hitView.frame.size.width, | |
height: hitView.frame.size.height | |
) | |
singlePressValue = SinglePressValue( | |
viewWireframe: .init( | |
rect: globalRect, | |
cornerRadius: hitView.layer.cornerRadius, | |
maskedCorners: hitView.layer.maskedCorners | |
) | |
) | |
setNeedsDisplay() | |
} | |
} else if sender.state == .ended { | |
singlePressValue = nil | |
setNeedsDisplay() | |
} | |
} | |
/// 2点で長押しした際に呼ばれる関数 | |
@objc private func didDoubleLongPress(_ sender: UILongPressGestureRecognizer) { | |
if sender.state == .began { | |
if sender.numberOfTouches != 2 { return } | |
let position1InWindow = sender.location(ofTouch: 0, in: window) | |
let position2InWindow = sender.location(ofTouch: 1, in: window) | |
if let hitView1 = window?.hitTest(position1InWindow, with: nil), let hitView2 = window?.hitTest(position2InWindow, with: nil) { | |
let positionInHitView1 = sender.location(ofTouch: 0, in: hitView1) | |
let positionInHitView2 = sender.location(ofTouch: 1, in: hitView2) | |
let globalRect1 = CGRect( | |
x: position1InWindow.x - positionInHitView1.x, | |
y: position1InWindow.y - positionInHitView1.y, | |
width: hitView1.frame.size.width, | |
height: hitView1.frame.size.height | |
) | |
let globalRect2 = CGRect( | |
x: position2InWindow.x - positionInHitView2.x, | |
y: position2InWindow.y - positionInHitView2.y, | |
width: hitView2.frame.size.width, | |
height: hitView2.frame.size.height | |
) | |
doublePressValue = DoublePressValue( | |
baseViewWireframe: .init( | |
rect: globalRect1, | |
cornerRadius: hitView1.layer.cornerRadius, | |
maskedCorners: hitView1.layer.maskedCorners | |
), | |
targetViewWireframe: .init( | |
rect: globalRect2, | |
cornerRadius: hitView2.layer.cornerRadius, | |
maskedCorners: hitView2.layer.maskedCorners | |
) | |
) | |
setNeedsDisplay() | |
} | |
} else if sender.state == .ended { | |
doublePressValue = nil | |
setNeedsDisplay() | |
} | |
} | |
/// View間のマージン描画用関数 | |
private func drawComponentMarginLine(baseRect: CGRect, targetRect: CGRect) { | |
let isTargetFrameOnTheRight = (targetRect.minX - baseRect.maxX) >= 0 | |
let isTargetFrameOnTheLeft = (baseRect.minX - targetRect.maxX) >= 0 | |
let isTargetFrameOnTheBottom = (targetRect.minY - baseRect.maxY) >= 0 | |
let isTargetFrameOnTheTop = (baseRect.minY - targetRect.maxY) >= 0 | |
let isMidXCoveredTargetFrame = targetRect.minX <= baseRect.midX && targetRect.maxX >= baseRect.midX | |
let isMidYCoveredTargetFrame = targetRect.minY <= baseRect.midY && targetRect.maxY >= baseRect.midY | |
let deltaX = targetRect.midX - baseRect.midX | |
let deltaY = targetRect.midY - baseRect.midY | |
// 実線のみ | |
let top = isMidXCoveredTargetFrame && isTargetFrameOnTheTop | |
let bottom = isMidXCoveredTargetFrame && isTargetFrameOnTheBottom | |
let leading = isMidYCoveredTargetFrame && isTargetFrameOnTheLeft | |
let trailing = isMidYCoveredTargetFrame && isTargetFrameOnTheRight | |
// 前半部分が実線の方向、後半部分が破線の方向 | |
let topLeading = !isMidXCoveredTargetFrame && isTargetFrameOnTheTop && deltaX < 0 | |
let topTrailing = !isMidXCoveredTargetFrame && isTargetFrameOnTheTop && deltaX >= 0 | |
let bottomLeading = !isMidXCoveredTargetFrame && isTargetFrameOnTheBottom && deltaX < 0 | |
let bottomTrailing = !isMidXCoveredTargetFrame && isTargetFrameOnTheBottom && deltaX >= 0 | |
let leadingTop = !isMidYCoveredTargetFrame && isTargetFrameOnTheLeft && deltaY < 0 | |
let leadingBottom = !isMidYCoveredTargetFrame && isTargetFrameOnTheLeft && deltaY >= 0 | |
let trailingTop = !isMidYCoveredTargetFrame && isTargetFrameOnTheRight && deltaY < 0 | |
let trailingBottom = !isMidYCoveredTargetFrame && isTargetFrameOnTheRight && deltaY >= 0 | |
// 実線 (上) | |
if top || topLeading || topTrailing { | |
drawVerticalLine( | |
startY: baseRect.minY, | |
endY: targetRect.maxY, | |
positionX: baseRect.midX, | |
color: AccentColor.red, | |
isDash: false, | |
showsTooltip: true | |
) | |
} | |
// 実線 (下) | |
if bottom || bottomLeading || bottomTrailing { | |
drawVerticalLine( | |
startY: baseRect.maxY, | |
endY: targetRect.minY, | |
positionX: baseRect.midX, | |
color: AccentColor.red, | |
isDash: false, | |
showsTooltip: true | |
) | |
} | |
// 実線 (左) | |
if leading || leadingTop || leadingBottom { | |
drawHorizontalLine( | |
startX: baseRect.minX, | |
endX: targetRect.maxX, | |
positionY: baseRect.midY, | |
color: AccentColor.red, | |
isDash: false, | |
showsTooltip: true | |
) | |
} | |
// 実線 (右) | |
if trailing || trailingTop || trailingBottom { | |
drawHorizontalLine( | |
startX: baseRect.maxX, | |
endX: targetRect.minX, | |
positionY: baseRect.midY, | |
color: AccentColor.red, | |
isDash: false, | |
showsTooltip: true | |
) | |
} | |
// 破線 (上方向の実線から左へ) | |
if topLeading { | |
drawHorizontalLine( | |
startX: baseRect.midX, | |
endX: targetRect.maxX, | |
positionY: targetRect.maxY, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (上方向の実線から右へ) | |
if topTrailing { | |
drawHorizontalLine( | |
startX: baseRect.midX, | |
endX: targetRect.minX, | |
positionY: targetRect.maxY, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (下方向の実線から左へ) | |
if bottomLeading { | |
drawHorizontalLine( | |
startX: baseRect.midX, | |
endX: targetRect.maxX, | |
positionY: targetRect.minY, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (下方向の実線から右へ) | |
if bottomTrailing { | |
drawHorizontalLine( | |
startX: baseRect.midX, | |
endX: targetRect.minX, | |
positionY: targetRect.minY, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (左方向の実線から上へ) | |
if leadingTop { | |
drawVerticalLine( | |
startY: baseRect.midY, | |
endY: targetRect.maxY, | |
positionX: targetRect.maxX, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (左方向の実線から下へ) | |
if leadingBottom { | |
drawVerticalLine( | |
startY: baseRect.midY, | |
endY: targetRect.minY, | |
positionX: targetRect.maxX, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (右方向の実線から上へ) | |
if trailingTop { | |
drawVerticalLine( | |
startY: baseRect.midY, | |
endY: targetRect.maxY, | |
positionX: targetRect.minX, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
// 破線 (右方向の実線から下へ) | |
if trailingBottom { | |
drawVerticalLine( | |
startY: baseRect.midY, | |
endY: targetRect.minY, | |
positionX: targetRect.minX, | |
color: AccentColor.red, | |
isDash: true, | |
showsTooltip: false | |
) | |
} | |
} | |
/// Viewのフレーム描画用関数 | |
private func drawViewWireframe(_ viewWireframe: ViewWireframe, shouldShowScreenMargin: Bool, shouldShowCorner: Bool) { | |
// Viewのフレーム表示 | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(AccentColor.purple) | |
context.setLineWidth(0.5) | |
context.addRect(viewWireframe.rect) | |
context.strokePath() | |
} | |
// Viewのフレームサイズ表示 | |
let formattedWidth = numberFormatter.string(from: NSNumber(value: viewWireframe.rect.width)) ?? "" | |
let formattedHeight = numberFormatter.string(from: NSNumber(value: viewWireframe.rect.height)) ?? "" | |
drawTextTooltip( | |
text: "\(formattedWidth)×\(formattedHeight)", | |
maxSize: .init(width: 64, height: 20), | |
anchorPoint: .init(x: viewWireframe.rect.midX, y: viewWireframe.rect.maxY + 5), | |
color: AccentColor.purple | |
) | |
// スクリーンとのマージン表示 | |
if let window, shouldShowScreenMargin { | |
drawOuterMargin( | |
baseRect: viewWireframe.rect, | |
targetRect: window.frame | |
) | |
} | |
// 角丸表示 | |
if shouldShowCorner { | |
drawCornerRadius(viewWireframe: viewWireframe) | |
} | |
} | |
/// 角丸の描画用関数 | |
private func drawCornerRadius(viewWireframe: ViewWireframe) { | |
if viewWireframe.cornerRadius == .zero { return } | |
let ellipseSize = CGSize(width: 6, height: 6) | |
// 左上 | |
if viewWireframe.maskedCorners.contains(.layerMinXMinYCorner) { | |
drawing(mode: .fillStroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setFillColor(UIColor.white.cgColor) | |
context.setLineWidth(1.0) | |
context.addEllipse(in: .init( | |
x: viewWireframe.rect.minX + max(ellipseSize.width, viewWireframe.cornerRadius / 2) - ellipseSize.width / 2, | |
y: viewWireframe.rect.minY + max(ellipseSize.height, viewWireframe.cornerRadius / 2) - ellipseSize.height / 2, | |
width: ellipseSize.width, | |
height: ellipseSize.height | |
)) | |
context.drawPath(using: .fillStroke) | |
} | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setLineWidth(2.0) | |
context.addArc( | |
center: .init(x: viewWireframe.rect.minX + viewWireframe.cornerRadius, y: viewWireframe.rect.minY + viewWireframe.cornerRadius), | |
radius: viewWireframe.cornerRadius, | |
startAngle: radians(180), | |
endAngle: radians(270), | |
clockwise: false | |
) | |
context.strokePath() | |
} | |
drawTextTooltip( | |
text: "\(viewWireframe.cornerRadius)", | |
maxSize: .init(width: 24, height: 20), | |
anchorPoint: .init(x: viewWireframe.rect.minX - 10, y: viewWireframe.rect.minY - 20), | |
color: AccentColor.green | |
) | |
} | |
// 右上 | |
if viewWireframe.maskedCorners.contains(.layerMinXMaxYCorner) { | |
drawing(mode: .fillStroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setFillColor(UIColor.white.cgColor) | |
context.setLineWidth(1.0) | |
context.addEllipse(in: .init( | |
x: viewWireframe.rect.maxX - max(ellipseSize.width, viewWireframe.cornerRadius / 2) - ellipseSize.width / 2, | |
y: viewWireframe.rect.minY + max(ellipseSize.height, viewWireframe.cornerRadius / 2) - ellipseSize.height / 2, | |
width: ellipseSize.width, | |
height: ellipseSize.height | |
)) | |
context.drawPath(using: .fillStroke) | |
} | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setLineWidth(2.0) | |
context.addArc( | |
center: .init(x: viewWireframe.rect.maxX - viewWireframe.cornerRadius, y: viewWireframe.rect.minY + viewWireframe.cornerRadius), | |
radius: viewWireframe.cornerRadius, | |
startAngle: radians(270), | |
endAngle: radians(360), | |
clockwise: false | |
) | |
context.strokePath() | |
} | |
drawTextTooltip( | |
text: "\(viewWireframe.cornerRadius)", | |
maxSize: .init(width: 24, height: 20), | |
anchorPoint: .init(x: viewWireframe.rect.maxX + 10, y: viewWireframe.rect.minY - 20), | |
color: AccentColor.green | |
) | |
} | |
// 左下 | |
if viewWireframe.maskedCorners.contains(.layerMaxXMinYCorner) { | |
drawing(mode: .fillStroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setFillColor(UIColor.white.cgColor) | |
context.setLineWidth(1.0) | |
context.addEllipse(in: .init( | |
x: viewWireframe.rect.minX + max(ellipseSize.width, viewWireframe.cornerRadius / 2) - ellipseSize.width / 2, | |
y: viewWireframe.rect.maxY - max(ellipseSize.height, viewWireframe.cornerRadius / 2) - ellipseSize.height / 2, | |
width: ellipseSize.width, | |
height: ellipseSize.height | |
)) | |
context.drawPath(using: .fillStroke) | |
} | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setLineWidth(2.0) | |
context.addArc( | |
center: .init(x: viewWireframe.rect.minX + viewWireframe.cornerRadius, y: viewWireframe.rect.maxY - viewWireframe.cornerRadius), | |
radius: viewWireframe.cornerRadius, | |
startAngle: radians(90), | |
endAngle: radians(180), | |
clockwise: false | |
) | |
context.strokePath() | |
} | |
drawTextTooltip( | |
text: "\(viewWireframe.cornerRadius)", | |
maxSize: .init(width: 24, height: 20), | |
anchorPoint: .init(x: viewWireframe.rect.minX - 10, y: viewWireframe.rect.maxY + 2), | |
color: AccentColor.green | |
) | |
} | |
// 右下 | |
if viewWireframe.maskedCorners.contains(.layerMaxXMaxYCorner) { | |
drawing(mode: .fillStroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setFillColor(UIColor.white.cgColor) | |
context.setLineWidth(1.0) | |
context.addEllipse(in: .init( | |
x: viewWireframe.rect.maxX - max(ellipseSize.height, viewWireframe.cornerRadius / 2) - ellipseSize.width / 2, | |
y: viewWireframe.rect.maxY - max(ellipseSize.height, viewWireframe.cornerRadius / 2) - ellipseSize.height / 2, | |
width: ellipseSize.width, | |
height: ellipseSize.height | |
)) | |
context.drawPath(using: .fillStroke) | |
} | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(AccentColor.green) | |
context.setLineWidth(2.0) | |
context.addArc( | |
center: .init(x: viewWireframe.rect.maxX - viewWireframe.cornerRadius, y: viewWireframe.rect.maxY - viewWireframe.cornerRadius), | |
radius: viewWireframe.cornerRadius, | |
startAngle: radians(0), | |
endAngle: radians(90), | |
clockwise: false | |
) | |
context.strokePath() | |
} | |
drawTextTooltip( | |
text: "\(viewWireframe.cornerRadius)", | |
maxSize: .init(width: 24, height: 20), | |
anchorPoint: .init(x: viewWireframe.rect.maxX + 10, y: viewWireframe.rect.maxY + 2), | |
color: AccentColor.green | |
) | |
} | |
} | |
/// 対象と外界のマージン描画用関数 | |
private func drawOuterMargin(baseRect: CGRect, targetRect: CGRect) { | |
// 対象のフレームがベースのフレームを覆ってない場合は描画しない | |
if !targetRect.contains(baseRect) { return } | |
// 左端のマージン | |
drawHorizontalLine(startX: baseRect.minX, endX: targetRect.minX, positionY: baseRect.midY, color: AccentColor.blue, isDash: true, showsTooltip: true) | |
// 右端のマージン | |
drawHorizontalLine(startX: baseRect.maxX, endX: targetRect.maxX, positionY: baseRect.midY, color: AccentColor.blue, isDash: true, showsTooltip: true) | |
// 上端のマージン | |
drawVerticalLine(startY: baseRect.minY, endY: targetRect.minY, positionX: baseRect.midX, color: AccentColor.blue, isDash: true, showsTooltip: true) | |
// 下端のマージン | |
drawVerticalLine(startY: baseRect.maxY, endY: targetRect.maxY, positionX: baseRect.midX, color: AccentColor.blue, isDash: true, showsTooltip: true) | |
} | |
/// 縦線の描画用関数 | |
private func drawVerticalLine(startY: CGFloat, endY: CGFloat, positionX: CGFloat, color: CGColor, isDash: Bool, showsTooltip: Bool) { | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(color) | |
context.setLineWidth(0.5) | |
if isDash { | |
context.setLineDash(phase: 0, lengths: [2.0, 2.0]) | |
} | |
context.move(to: CGPoint(x: positionX, y: startY)) | |
context.addLine(to: CGPoint(x: positionX, y: endY)) | |
context.strokePath() | |
} | |
if showsTooltip { | |
let bottomText = numberFormatter.string(from: NSNumber(value: abs(endY - startY))) ?? "" | |
drawTextTooltip( | |
text: "\(bottomText)", | |
maxSize: .init(width: 40, height: 20), | |
anchorPoint: .init(x: positionX + 5, y: (startY + endY) / 2), | |
color: color | |
) | |
} | |
} | |
/// 横線の描画用関数 | |
private func drawHorizontalLine(startX: CGFloat, endX: CGFloat, positionY: CGFloat, color: CGColor, isDash: Bool, showsTooltip: Bool) { | |
drawing(mode: .stroke) { context in | |
context.setStrokeColor(color) | |
context.setLineWidth(0.5) | |
if isDash { | |
context.setLineDash(phase: 0, lengths: [2.0, 2.0]) | |
} | |
context.move(to: CGPoint(x: startX, y: positionY)) | |
context.addLine(to: CGPoint(x: endX, y: positionY)) | |
context.strokePath() | |
} | |
if showsTooltip { | |
let rightText = numberFormatter.string(from: NSNumber(value: abs(endX - startX))) ?? "" | |
drawTextTooltip( | |
text: "\(rightText)", | |
maxSize: .init(width: 40, height: 20), | |
anchorPoint: .init(x: (startX + endX) / 2, y: positionY - 25), | |
color: color | |
) | |
} | |
} | |
/// ツールチップの描画用関数 | |
private func drawTextTooltip(text: String, maxSize: CGSize, anchorPoint: CGPoint, color: CGColor) { | |
drawing(mode: .fill) { context in | |
let attributedText = NSAttributedString(string: text, attributes: [ | |
.foregroundColor: UIColor.white, | |
.font: UIFont.systemFont(ofSize: 8), | |
]) | |
let typesetter = CTTypesetterCreateWithAttributedString(attributedText) | |
let ctLine = CTTypesetterCreateLine(typesetter, CFRangeMake(0, attributedText.length)) | |
let framesetter = CTFramesetterCreateWithAttributedString(attributedText) | |
let innerMargin: CGFloat = 4 | |
let suggestFrameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, CGSize(width: maxSize.width - innerMargin, height: maxSize.height - innerMargin), nil) | |
let baseSize = CGSize(width: suggestFrameSize.width + innerMargin * 2, height: suggestFrameSize.height + innerMargin * 2) | |
let frame = CGRect(x: anchorPoint.x - baseSize.width / 2, y: anchorPoint.y, width: baseSize.width, height: baseSize.height) | |
context.setFillColor(color) | |
context.addPath(UIBezierPath(roundedRect: frame, cornerRadius: 2).cgPath) | |
context.fillPath() | |
context.textPosition = .zero | |
context.textMatrix = .identity | |
context.translateBy( | |
x: frame.origin.x + (frame.width - suggestFrameSize.width) / 2, | |
y: frame.origin.y + (frame.height - suggestFrameSize.height) / 2 + suggestFrameSize.height | |
) | |
context.scaleBy(x: 1, y: -1) | |
CTLineDraw(ctLine, context) | |
} | |
} | |
/// 度→ラジアンへの変換用関数 | |
private func radians(_ degrees: CGFloat) -> CGFloat { | |
let radian = (CGFloat.pi * 2) * (degrees / 360.0) | |
return radian | |
} | |
/// 描画用の関数 (他の描画箇所に影響を起こさないようにCGContextの状態をclosure内に留めるために用意しています) | |
private func drawing(mode: CGPathDrawingMode, closure: (_ context: CGContext) -> Void) { | |
guard let context = UIGraphicsGetCurrentContext() else { return } | |
context.saveGState() | |
closure(context) | |
context.restoreGState() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment