647 lines
21 KiB
Swift
647 lines
21 KiB
Swift
//
|
|
// PigeonWatermarkCameraView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import AVFoundation
|
|
|
|
final class PigeonWatermarkCameraView: UIView {
|
|
|
|
var onClose: (() -> Void)?
|
|
var onConfirm: ((UIImage) -> Void)?
|
|
|
|
private let session = AVCaptureSession()
|
|
private let sessionQueue = DispatchQueue(label: "pigeon.watermark.camera")
|
|
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
private var videoInput: AVCaptureDeviceInput?
|
|
private var photoOutput: AVCapturePhotoOutput?
|
|
|
|
private var cameraPosition: AVCaptureDevice.Position = .back
|
|
private var isTorchOn = false
|
|
private var isReviewing = false
|
|
private var isCapturing = false
|
|
private var didConfigureSession = false
|
|
private var capturedImage: UIImage?
|
|
private var captureDate = Date()
|
|
private var addressText = ""
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
setupUI()
|
|
applyReviewState(false)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
let session = self.session
|
|
sessionQueue.async {
|
|
if session.isRunning {
|
|
session.stopRunning()
|
|
}
|
|
}
|
|
}
|
|
|
|
func startSession() {
|
|
requestAccessAndConfigure()
|
|
}
|
|
|
|
func stopSession() {
|
|
let session = self.session
|
|
sessionQueue.async {
|
|
if session.isRunning {
|
|
session.stopRunning()
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateAddress(_ address: String) {
|
|
addressText = address
|
|
if isReviewing {
|
|
refreshWatermark(at: captureDate)
|
|
}
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
previewLayer?.frame = previewHost.bounds
|
|
shutterButton.setNeedsDisplay()
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(gradientView)
|
|
addSubview(navView)
|
|
addSubview(previewContainer)
|
|
previewContainer.addSubview(previewHost)
|
|
previewContainer.addSubview(capturedImageView)
|
|
previewContainer.addSubview(watermarkView)
|
|
addSubview(flashButton)
|
|
addSubview(shutterButton)
|
|
addSubview(switchButton)
|
|
addSubview(retakeButton)
|
|
addSubview(confirmButton)
|
|
|
|
gradientView.layoutChain
|
|
.top()
|
|
.edgesHorzontal()
|
|
.height(135)
|
|
|
|
navView.layoutChain.edges(excludingEdge: .bottom).height(kNaviHeight)
|
|
navView.onBackTapped = { [weak self] in
|
|
self?.onClose?()
|
|
}
|
|
|
|
previewContainer.layoutChain
|
|
.top(kNaviHeight + 73)
|
|
.edgesHorzontal(30)
|
|
.heightToWidth(424/315)
|
|
|
|
previewHost.layoutChain.edges()
|
|
capturedImageView.layoutChain.edges()
|
|
|
|
watermarkView.layoutChain
|
|
.left(12)
|
|
.top(12)
|
|
watermarkView.rightAnchor.constraint(
|
|
lessThanOrEqualTo: previewContainer.rightAnchor,
|
|
constant: -12
|
|
).isActive = true
|
|
|
|
shutterButton.layoutChain
|
|
.centerX()
|
|
.bottom(kSafeBottomMargin + 18)
|
|
.width(84)
|
|
.height(84)
|
|
|
|
flashButton.layoutChain
|
|
.centerY(shutterButton)
|
|
.rightToLeftOfView(shutterButton, offset: -52)
|
|
.width(50)
|
|
.height(50)
|
|
|
|
switchButton.layoutChain
|
|
.centerY(shutterButton)
|
|
.leftToRightOfView(shutterButton, offset: 52)
|
|
.width(50)
|
|
.height(50)
|
|
|
|
retakeButton.layoutChain
|
|
.left(30)
|
|
.bottom(kSafeBottomMargin + 24)
|
|
.height(50)
|
|
.width(110)
|
|
|
|
confirmButton.layoutChain
|
|
.leftToRightOfView(retakeButton, offset: 16)
|
|
.right(30)
|
|
.bottomToView(retakeButton)
|
|
.height(50)
|
|
}
|
|
|
|
func requestAccessAndConfigure() {
|
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
case .authorized:
|
|
configureSession()
|
|
case .notDetermined:
|
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
|
DispatchQueue.main.async {
|
|
if granted {
|
|
self?.configureSession()
|
|
} else {
|
|
DLToast.show(text: "请在设置中开启相机权限")
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
DLToast.show(text: "请在设置中开启相机权限")
|
|
}
|
|
}
|
|
|
|
private func configureSession() {
|
|
sessionQueue.async { [weak self] in
|
|
guard let self else { return }
|
|
if !self.didConfigureSession {
|
|
self.session.beginConfiguration()
|
|
self.session.sessionPreset = .photo
|
|
_ = self.attachVideoInput(position: self.cameraPosition)
|
|
let output = AVCapturePhotoOutput()
|
|
if self.session.canAddOutput(output) {
|
|
self.session.addOutput(output)
|
|
self.photoOutput = output
|
|
}
|
|
self.session.commitConfiguration()
|
|
self.didConfigureSession = true
|
|
DispatchQueue.main.async {
|
|
let layer = AVCaptureVideoPreviewLayer(session: self.session)
|
|
layer.videoGravity = .resizeAspectFill
|
|
layer.frame = self.previewHost.bounds
|
|
self.previewHost.layer.insertSublayer(layer, at: 0)
|
|
self.previewLayer = layer
|
|
self.applyVideoOrientation()
|
|
}
|
|
}
|
|
self.applyVideoOrientation()
|
|
if !self.session.isRunning {
|
|
self.session.startRunning()
|
|
}
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
private func attachVideoInput(position: AVCaptureDevice.Position) -> Bool {
|
|
if let videoInput {
|
|
session.removeInput(videoInput)
|
|
self.videoInput = nil
|
|
}
|
|
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position),
|
|
let input = try? AVCaptureDeviceInput(device: device),
|
|
session.canAddInput(input) else { return false }
|
|
session.addInput(input)
|
|
videoInput = input
|
|
cameraPosition = position
|
|
return true
|
|
}
|
|
|
|
private func applyVideoOrientation() {
|
|
if let conn = photoOutput?.connection(with: .video), conn.isVideoOrientationSupported {
|
|
conn.videoOrientation = .portrait
|
|
if conn.isVideoMirroringSupported {
|
|
conn.automaticallyAdjustsVideoMirroring = false
|
|
conn.isVideoMirrored = cameraPosition == .front
|
|
}
|
|
}
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self, let conn = self.previewLayer?.connection, conn.isVideoOrientationSupported else { return }
|
|
conn.videoOrientation = .portrait
|
|
}
|
|
}
|
|
|
|
@objc private func tapFlash() {
|
|
guard !isReviewing else { return }
|
|
guard cameraPosition == .back, let device = videoInput?.device, device.hasTorch else {
|
|
DLToast.show(text: "当前镜头不支持闪光灯")
|
|
return
|
|
}
|
|
do {
|
|
try device.lockForConfiguration()
|
|
isTorchOn.toggle()
|
|
device.torchMode = isTorchOn ? .on : .off
|
|
device.unlockForConfiguration()
|
|
flashButton.alpha = isTorchOn ? 1 : 0.72
|
|
} catch {
|
|
DLToast.show(text: "闪光灯开启失败")
|
|
}
|
|
}
|
|
|
|
@objc private func tapSwitch() {
|
|
guard !isReviewing else { return }
|
|
let next: AVCaptureDevice.Position = cameraPosition == .back ? .front : .back
|
|
sessionQueue.async { [weak self] in
|
|
guard let self else { return }
|
|
self.session.beginConfiguration()
|
|
_ = self.attachVideoInput(position: next)
|
|
if self.isTorchOn, next == .front {
|
|
self.isTorchOn = false
|
|
}
|
|
self.applyVideoOrientation()
|
|
self.session.commitConfiguration()
|
|
DispatchQueue.main.async {
|
|
self.flashButton.alpha = self.isTorchOn ? 1 : 0.72
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func tapShutter() {
|
|
guard !isReviewing, !isCapturing, let photoOutput else { return }
|
|
isCapturing = true
|
|
let settings = AVCapturePhotoSettings()
|
|
if let device = videoInput?.device, device.hasFlash, cameraPosition == .back {
|
|
settings.flashMode = isTorchOn ? .on : .off
|
|
}
|
|
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
}
|
|
|
|
@objc private func tapRetake() {
|
|
capturedImage = nil
|
|
capturedImageView.image = nil
|
|
isReviewing = false
|
|
isCapturing = false
|
|
applyReviewState(false)
|
|
}
|
|
|
|
@objc private func tapConfirm() {
|
|
guard let image = makeWatermarkedImage() else { return }
|
|
onConfirm?(image)
|
|
}
|
|
|
|
private func applyReviewState(_ reviewing: Bool) {
|
|
capturedImageView.isHidden = !reviewing
|
|
watermarkView.isHidden = !reviewing
|
|
flashButton.isHidden = reviewing
|
|
shutterButton.isHidden = reviewing
|
|
switchButton.isHidden = reviewing
|
|
retakeButton.isHidden = !reviewing
|
|
confirmButton.isHidden = !reviewing
|
|
}
|
|
|
|
private func refreshWatermark(at date: Date) {
|
|
let timeFormatter = DateFormatter()
|
|
timeFormatter.locale = Locale(identifier: "zh_CN")
|
|
timeFormatter.dateFormat = "HH:mm"
|
|
timeLabel.text = timeFormatter.string(from: date)
|
|
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.locale = Locale(identifier: "zh_CN")
|
|
dateFormatter.dateFormat = "yyyy-M-d"
|
|
dateLabel.text = dateFormatter.string(from: date)
|
|
|
|
let weekday = Calendar(identifier: .gregorian).component(.weekday, from: date)
|
|
let names = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]
|
|
weekdayLabel.text = names[max(0, min(6, weekday - 1))]
|
|
|
|
let hasAddress = !addressText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
locationRow.isHidden = !hasAddress
|
|
addressLabel.text = addressText
|
|
}
|
|
|
|
private func makeWatermarkedImage() -> UIImage? {
|
|
guard let capturedImage else { return nil }
|
|
layoutIfNeeded()
|
|
let target = previewContainer.bounds.size
|
|
let watermarkSize = watermarkView.bounds.size
|
|
let watermarkFrame = watermarkView.frame
|
|
guard target.width > 1, target.height > 1, watermarkSize.width > 1 else { return capturedImage }
|
|
|
|
watermarkBlurView.isHidden = true
|
|
watermarkTintView.isHidden = true
|
|
let borderWidth = watermarkView.layer.borderWidth
|
|
watermarkView.layer.borderWidth = 0
|
|
layoutIfNeeded()
|
|
|
|
let scale = UIScreen.main.scale
|
|
let clearFormat = UIGraphicsImageRendererFormat.default()
|
|
clearFormat.scale = scale
|
|
clearFormat.opaque = false
|
|
let contentImage = UIGraphicsImageRenderer(size: watermarkSize, format: clearFormat).image { ctx in
|
|
ctx.cgContext.clear(CGRect(origin: .zero, size: watermarkSize))
|
|
watermarkView.drawHierarchy(
|
|
in: CGRect(origin: .zero, size: watermarkSize),
|
|
afterScreenUpdates: true
|
|
)
|
|
}
|
|
|
|
watermarkBlurView.isHidden = false
|
|
watermarkTintView.isHidden = false
|
|
watermarkView.layer.borderWidth = borderWidth
|
|
|
|
let photoFormat = UIGraphicsImageRendererFormat.default()
|
|
photoFormat.scale = scale
|
|
photoFormat.opaque = true
|
|
return UIGraphicsImageRenderer(size: target, format: photoFormat).image { _ in
|
|
Self.drawAspectFill(capturedImage, in: CGRect(origin: .zero, size: target))
|
|
let card = UIBezierPath(roundedRect: watermarkFrame, cornerRadius: 14)
|
|
UIColor.white.withAlphaComponent(0.28).setFill()
|
|
card.fill()
|
|
UIColor.white.setStroke()
|
|
card.lineWidth = 1
|
|
card.stroke()
|
|
contentImage.draw(in: watermarkFrame)
|
|
}
|
|
}
|
|
|
|
private static func drawAspectFill(_ image: UIImage, in rect: CGRect) {
|
|
let imgSize = image.size
|
|
guard imgSize.width > 0, imgSize.height > 0 else { return }
|
|
let scale = max(rect.width / imgSize.width, rect.height / imgSize.height)
|
|
let scaled = CGSize(width: imgSize.width * scale, height: imgSize.height * scale)
|
|
let origin = CGPoint(
|
|
x: rect.midX - scaled.width / 2,
|
|
y: rect.midY - scaled.height / 2
|
|
)
|
|
image.draw(in: CGRect(origin: origin, size: scaled))
|
|
}
|
|
|
|
private lazy var gradientView: PigeonCameraGradientView = {
|
|
let view = PigeonCameraGradientView()
|
|
view.isUserInteractionEnabled = false
|
|
return view
|
|
}()
|
|
|
|
private lazy var navView: BaseNavigationView = {
|
|
BaseNavigationView(title: "水印拍照")
|
|
}()
|
|
|
|
private lazy var previewContainer: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor(hexStr: "#DDE1E5")
|
|
view.layer.cornerRadius = 24
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var previewHost: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .black
|
|
return view
|
|
}()
|
|
|
|
private lazy var capturedImageView: UIImageView = {
|
|
let view = UIImageView()
|
|
view.contentMode = .scaleAspectFill
|
|
view.clipsToBounds = true
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
private lazy var watermarkBlurView: UIVisualEffectView = {
|
|
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight))
|
|
view.isUserInteractionEnabled = false
|
|
return view
|
|
}()
|
|
|
|
private lazy var watermarkTintView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor.white.withAlphaComponent(0.08)
|
|
view.isOpaque = false
|
|
view.isUserInteractionEnabled = false
|
|
return view
|
|
}()
|
|
|
|
private lazy var timeChipView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = UIColor(hexStr: "#00ADFE")
|
|
view.layer.cornerRadius = 10
|
|
|
|
let logo = UIView()
|
|
logo.backgroundColor = .white
|
|
logo.layer.cornerRadius = 6
|
|
|
|
timeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
|
timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
|
|
view.addSubview(logo)
|
|
view.addSubview(timeLabel)
|
|
logo.layoutChain
|
|
.left(6)
|
|
.centerY()
|
|
.width(22)
|
|
.height(22)
|
|
timeLabel.layoutChain
|
|
.leftToRightOfView(logo, offset: 6)
|
|
.right(8)
|
|
.top(6)
|
|
.bottom(6)
|
|
return view
|
|
}()
|
|
|
|
private lazy var watermarkView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .clear
|
|
view.isOpaque = false
|
|
view.layer.cornerRadius = 14
|
|
view.layer.borderWidth = 1
|
|
view.layer.borderColor = UIColor.white.cgColor
|
|
view.clipsToBounds = true
|
|
view.isHidden = true
|
|
|
|
view.addSubview(watermarkBlurView)
|
|
view.addSubview(watermarkTintView)
|
|
watermarkBlurView.layoutChain.edges()
|
|
watermarkTintView.layoutChain.edges()
|
|
|
|
let dateColumn = UIStackView(arrangedSubviews: [dateLabel, weekdayLabel])
|
|
dateColumn.axis = .vertical
|
|
dateColumn.spacing = 1
|
|
|
|
let topRow = UIStackView(arrangedSubviews: [timeChipView, dateColumn])
|
|
topRow.axis = .horizontal
|
|
topRow.alignment = .center
|
|
topRow.spacing = 8
|
|
|
|
let stack = UIStackView(arrangedSubviews: [topRow, locationRow])
|
|
stack.axis = .vertical
|
|
stack.alignment = .fill
|
|
stack.spacing = 8
|
|
view.addSubview(stack)
|
|
stack.layoutChain
|
|
.top(10)
|
|
.left(10)
|
|
.right(12)
|
|
.bottom(10)
|
|
return view
|
|
}()
|
|
|
|
private lazy var timeLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 22, weight: .semibold)
|
|
label.textColor = .white
|
|
return label
|
|
}()
|
|
|
|
private lazy var dateLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
private lazy var weekdayLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
return label
|
|
}()
|
|
|
|
private lazy var addressLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
label.numberOfLines = 2
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}()
|
|
|
|
private lazy var locationRow: UIView = {
|
|
let row = UIView()
|
|
row.isHidden = true
|
|
let pin = UIImageView(image: UIImage(named: "PigeonMessage/sparkle"))
|
|
pin.contentMode = .scaleAspectFit
|
|
row.addSubview(pin)
|
|
row.addSubview(addressLabel)
|
|
pin.layoutChain
|
|
.left()
|
|
.top(1)
|
|
.width(11)
|
|
.height(11)
|
|
addressLabel.layoutChain
|
|
.leftToRightOfView(pin, offset: 4)
|
|
.right()
|
|
.top()
|
|
.bottom()
|
|
return row
|
|
}()
|
|
|
|
private lazy var flashButton: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setImage(UIImage(named: "PigeonMessage/flash"), for: .normal)
|
|
button.alpha = 0.72
|
|
button.addTarget(self, action: #selector(tapFlash), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
|
|
private lazy var switchButton: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setImage(UIImage(named: "PigeonMessage/camera_switch"), for: .normal)
|
|
button.addTarget(self, action: #selector(tapSwitch), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
|
|
private lazy var shutterButton: PigeonShutterButton = {
|
|
let button = PigeonShutterButton()
|
|
button.addTarget(self, action: #selector(tapShutter), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
|
|
private lazy var retakeButton: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setTitle("重拍", for: .normal)
|
|
button.setTitleColor(UIColor(hexStr: "#13B6F1"), for: .normal)
|
|
button.titleLabel?.font = FontManager.boboBold(18)
|
|
button.backgroundColor = .white
|
|
button.layer.cornerRadius = 16
|
|
button.layer.borderWidth = 1.5
|
|
button.layer.borderColor = UIColor(hexStr: "#13B6F1").cgColor
|
|
button.addTarget(self, action: #selector(tapRetake), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
|
|
private lazy var confirmButton: UIButton = {
|
|
let button = UIButton(type: .custom)
|
|
button.setTitle("确定", for: .normal)
|
|
button.setTitleColor(.white, for: .normal)
|
|
button.titleLabel?.font = FontManager.boboBold(18)
|
|
button.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
|
button.clipsToBounds = true
|
|
button.cornerRadius = 16
|
|
button.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside)
|
|
return button
|
|
}()
|
|
}
|
|
|
|
extension PigeonWatermarkCameraView: AVCapturePhotoCaptureDelegate {
|
|
func photoOutput(
|
|
_ output: AVCapturePhotoOutput,
|
|
didFinishProcessingPhoto photo: AVCapturePhoto,
|
|
error: Error?
|
|
) {
|
|
isCapturing = false
|
|
if let error {
|
|
DLToast.show(text: "拍照失败:\(error.localizedDescription)")
|
|
return
|
|
}
|
|
guard let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else {
|
|
DLToast.show(text: "拍照失败")
|
|
return
|
|
}
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self else { return }
|
|
self.captureDate = Date()
|
|
self.capturedImage = image
|
|
self.capturedImageView.image = image
|
|
self.refreshWatermark(at: self.captureDate)
|
|
self.isReviewing = true
|
|
self.applyReviewState(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class PigeonCameraGradientView: UIView {
|
|
override class var layerClass: AnyClass { CAGradientLayer.self }
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
guard let gradient = layer as? CAGradientLayer else { return }
|
|
gradient.colors = [
|
|
UIColor(hexStr: "#BAF4FF").cgColor,
|
|
UIColor(hexStr: "#BAF4FF", alpha: 0).cgColor
|
|
]
|
|
gradient.startPoint = CGPoint(x: 0.5, y: 0)
|
|
gradient.endPoint = CGPoint(x: 0.5, y: 1)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
private final class PigeonShutterButton: UIControl {
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
isOpaque = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
let color = UIColor(hexStr: "#13B6F1")
|
|
let ring = UIBezierPath(ovalIn: rect.insetBy(dx: 3, dy: 3))
|
|
color.setStroke()
|
|
ring.lineWidth = 4
|
|
ring.stroke()
|
|
color.setFill()
|
|
UIBezierPath(ovalIn: rect.insetBy(dx: 11, dy: 11)).fill()
|
|
}
|
|
}
|