From 3cd633c025b31159cdeddb008ab2455dd1866c88 Mon Sep 17 00:00:00 2001 From: Eude Lesperance Date: Sat, 19 Oct 2019 02:11:06 -0500 Subject: [PATCH] Render Icon to image and use them --- CardParts/Assets/AssetManager.swift | 179 ++++++++++++++++++ .../Card Parts/CardPartConfettiView.swift | 8 +- .../Card Parts/CardPartTitleView.swift | 17 +- .../src/Classes/CardsViewController.swift | 3 +- Example/Tests/CardPartTitleViewTests.swift | 14 -- 5 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 CardParts/Assets/AssetManager.swift diff --git a/CardParts/Assets/AssetManager.swift b/CardParts/Assets/AssetManager.swift new file mode 100644 index 00000000..07c85bc9 --- /dev/null +++ b/CardParts/Assets/AssetManager.swift @@ -0,0 +1,179 @@ +// +// AssetManager.swift +// CardParts +// +// Created by Eude K Lesperance on 10/18/19. +// + +import UIKit + +class AssetManager { + static var shared = AssetManager() + + private var assets: [Icon: UIImage] = [:] + + init() { + Icon.allCases.forEach { icon in + assets[icon] = render(icon) + } + } + + func image(for icon: Icon) -> UIImage? { + return assets[icon] + } + + private func render(_ icon: Icon, in rect: CGRect = CGRect(x: 0, y: 0, width: 24.0, height: 24.0)) -> UIImage { + let format = UIGraphicsImageRendererFormat.default() + let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) + + let image = renderer.image { ctx in + ctx.cgContext.scaleBy(x: rect.width / 24.0, y: rect.height / 24.0) + let path = icon.bezierPath() + if icon == .confetti { + UIColor.white.setStroke() + path.stroke() + } else { + UIColor.white.setFill() + path.fill() + } + } + + return image.withRenderingMode(.alwaysTemplate) + } +} + +extension AssetManager { + enum Icon: CaseIterable { + case arrowDown, star, diamond, triangle, pencil, confetti + + func bezierPath() -> UIBezierPath { + switch self { + case .arrowDown: return arrowDown() + case .star: return star() + case .diamond: return diamond() + case .triangle: return triangle() + case .pencil: return pencil() + case .confetti: return confetti() + } + } + + private func arrowDown() -> UIBezierPath { + let path = UIBezierPath() + path.move(to: CGPoint(x: 7.41, y: 8.59)) + path.addLine(to: CGPoint(x: 12, y: 13.17)) + path.addLine(to: CGPoint(x: 16.59, y: 8.59)) + path.addLine(to: CGPoint(x: 18, y: 10)) + path.addLine(to: CGPoint(x: 12, y: 16)) + path.addLine(to: CGPoint(x: 6, y: 10)) + path.addLine(to: CGPoint(x: 7.41, y: 8.59)) + path.close() + return path + } + + private func star() -> UIBezierPath { + let size = CGSize(width: 24.0, height: 24.0) + let sides = 5 + let radius: CGFloat = min(size.width, size.height) / 2.0 + let origin = CGPoint(x: size.width / 2.0 , y: size.height / 2.0) + let pointiness: CGFloat = 2.0 + let startAngle = CGFloat(-1 * (360 / sides / 4)) + let adjustment = startAngle + CGFloat(360 / sides / 2) + + let points = polygonPoints(sides: sides, radius: radius / pointiness, origin: origin, adjustment: startAngle) + let points2 = polygonPoints(sides: sides, radius: radius, origin: origin, adjustment: adjustment) + + let path = UIBezierPath() + path.move(to: points[0]) + points.enumerated().forEach { i, point in + path.addLine(to: points2[i]) + path.addLine(to: point) + } + path.close() + return path + } + + private func confetti() -> UIBezierPath { + let linewidth: CGFloat = 4.0 + let size = CGSize(width: 24.0, height: 24.0) + let radius: CGFloat = (min(size.width, size.height) - linewidth) / 2.0 + let origin = CGPoint(x: size.width / 2.0 , y: size.height / 2.0) + let path = UIBezierPath(arcCenter: origin, radius: radius, startAngle: radian(from: 270), endAngle: radian(from: 180), clockwise: false) + path.lineWidth = linewidth + return path + } + + private func triangle() -> UIBezierPath { + let size = CGSize(width: 24.0, height: 24.0) + let sides = 3 + let radius: CGFloat = min(size.width, size.height) / 2.0 + let origin = CGPoint(x: size.width / 2.0 , y: size.height / 2.0) + let startAngle = CGFloat(-1 * (360 / sides / 4)) + let adjustment = startAngle + CGFloat(360 / sides / 2) + + let points = polygonPoints(sides: sides, radius: radius, origin: origin, adjustment: adjustment) + + let path = UIBezierPath() + path.move(to: points[0]) + points.enumerated().forEach { _, point in + print(point) + path.addLine(to: point) + } + return path + } + + private func diamond() -> UIBezierPath { + let path = UIBezierPath() + path.move(to: CGPoint(x: 11.96, y: 3.15)) + path.addLine(to: CGPoint(x: 11.85, y: 3)) + path.addLine(to: CGPoint(x: 5.24, y: 12.19)) + path.addLine(to: CGPoint(x: 5.17, y: 12.29)) + path.addLine(to: CGPoint(x: 12.24, y: 21.07)) + path.addLine(to: CGPoint(x: 12.27, y: 21.11)) + path.addLine(to: CGPoint(x: 18.88, y: 11.81)) + path.addLine(to: CGPoint(x: 18.95, y: 11.71)) + path.addLine(to: CGPoint(x: 11.96, y: 3.15)) + path.close() + return path + } + + private func pencil() -> UIBezierPath { + let path = UIBezierPath() + path.move(to: CGPoint(x: 3, y: 17.25)) + path.addLine(to: CGPoint(x: 3, y: 21)) + path.addLine(to: CGPoint(x: 6.75, y: 21)) + path.addLine(to: CGPoint(x: 17.81, y: 9.94)) + path.addLine(to: CGPoint(x: 14.06, y: 6.19)) + path.addLine(to: CGPoint(x: 3, y: 17.25)) + path.close() + path.move(to: CGPoint(x: 20.71, y: 7.04)) + path.addCurve(to: CGPoint(x: 20.71, y: 5.63), + controlPoint1: CGPoint(x: 21.1, y: 6.65), + controlPoint2: CGPoint(x: 21.1, y: 6.02)) + path.addLine(to: CGPoint(x: 18.37, y: 3.29)) + path.addCurve(to: CGPoint(x: 16.96, y: 3.29), + controlPoint1: CGPoint(x: 17.98, y: 2.9), + controlPoint2: CGPoint(x: 17.35, y: 2.9)) + path.addLine(to: CGPoint(x: 15.13, y: 5.12)) + path.addLine(to: CGPoint(x: 18.88, y: 8.87)) + path.addLine(to: CGPoint(x: 20.71, y: 7.04)) + path.close() + return path + } + + private func radian(from degree: CGFloat) -> CGFloat { + return CGFloat(Double.pi) * degree / 180.0 + } + + private func polygonPoints(sides: Int, radius: CGFloat, origin: CGPoint, adjustment: CGFloat = 0.0) -> [CGPoint] { + let angle = radian(from: 360 / CGFloat(sides)) + let points: [CGPoint] = (0...sides).reversed() + .map { side in + return CGPoint ( + x: origin.x - radius * cos(angle * CGFloat(side) + radian(from: adjustment)), + y: origin.y - radius * sin(angle * CGFloat(side) + radian(from: adjustment)) + ) + } + return points + } + } +} diff --git a/CardParts/src/Classes/Card Parts/CardPartConfettiView.swift b/CardParts/src/Classes/Card Parts/CardPartConfettiView.swift index b17d9853..d572bdd0 100644 --- a/CardParts/src/Classes/Card Parts/CardPartConfettiView.swift +++ b/CardParts/src/Classes/Card Parts/CardPartConfettiView.swift @@ -37,7 +37,7 @@ public class CardPartConfettiView: UIView, CardPartView { } } - public var confettiImages = [UIImage(named: "confetti", in: Bundle(for: CardPartConfettiView.self),compatibleWith: nil)] as? [UIImage] + public var confettiImages = [AssetManager.shared.image(for: .confetti)] as? [UIImage] //A layer that emits, animates, and renders a particle system. var emitter: CAEmitterLayer = CAEmitterLayer() @@ -127,15 +127,15 @@ public class CardPartConfettiView: UIView, CardPartView { private func image(for type: ConfettiType, index: Int = 0) -> UIImage? { switch type { case .diamond: - return UIImage(named: "diamond", in: Bundle(for: CardPartConfettiView.self), compatibleWith: nil) + return AssetManager.shared.image(for: .diamond) case .star: - return UIImage(named: "star", in: Bundle(for: CardPartConfettiView.self), compatibleWith: nil) + return AssetManager.shared.image(for: .star) case let .image(customImage): return customImage case .mixed: return confettiImages?[index] case .confetti: - return UIImage(named: "confetti", in: Bundle(for: CardPartConfettiView.self), compatibleWith: nil) + return AssetManager.shared.image(for: .confetti) } } } diff --git a/CardParts/src/Classes/Card Parts/CardPartTitleView.swift b/CardParts/src/Classes/Card Parts/CardPartTitleView.swift index b1a69c62..225e82e4 100644 --- a/CardParts/src/Classes/Card Parts/CardPartTitleView.swift +++ b/CardParts/src/Classes/Card Parts/CardPartTitleView.swift @@ -44,13 +44,6 @@ public class CardPartTitleView : UIView, CardPartView { } } } - public var menuButtonImageName: String = "arrowdown" { - didSet { - if type == .titleWithMenu || type == .titleWithActionableButton { - menuButtonImage = UIImage(named: menuButtonImageName, in: Bundle(for: CardPartTitleView.self), compatibleWith: nil) - } - } - } public var menuOptionObserver: ((String, Int) -> Void)? public var menuActionableCallback: (()->())? @@ -80,7 +73,7 @@ public class CardPartTitleView : UIView, CardPartView { if let image = menuButtonImage { button.setImage(image, for: .normal) } else { - button.setImage(UIImage(named: menuButtonImageName, in: Bundle(for: CardPartTitleView.self), compatibleWith: nil), for: .normal) + button.setImage(AssetManager.shared.image(for: .arrowDown), for: .normal) } button.addTarget(self, action: #selector(menuButtonTapped), for: UIControl.Event.touchUpInside) addSubview(button) @@ -92,7 +85,7 @@ public class CardPartTitleView : UIView, CardPartView { if let image = menuButtonImage { button.setImage(image, for: .normal) } else { - button.setImage(UIImage(named: menuButtonImageName, in: Bundle(for: CardPartTitleView.self), compatibleWith: nil), for: .normal) + button.setImage(AssetManager.shared.image(for: .arrowDown), for: .normal) } button.addTarget(self, action: #selector(actionableMenuTapped), for: UIControl.Event.touchUpInside) addSubview(button) @@ -190,12 +183,6 @@ extension Reactive where Base: CardPartTitleView { titleView.menuOptions = menuOptions } } - - public var menuButtonImageName: Binder{ - return Binder(self.base) { (titleView, menuButtonImageName) -> () in - titleView.menuButtonImageName = menuButtonImageName - } - } public var menuButtonImage: Binder{ return Binder(self.base) { (titleView, menuButtonImage) -> () in diff --git a/CardParts/src/Classes/CardsViewController.swift b/CardParts/src/Classes/CardsViewController.swift index c27f7d60..a0834f40 100644 --- a/CardParts/src/Classes/CardsViewController.swift +++ b/CardParts/src/Classes/CardsViewController.swift @@ -42,7 +42,6 @@ open class CardsViewController : UIViewController, UICollectionViewDataSource, U let editButtonOffset : CGFloat = 24 let editButtonHeight : CGFloat = 50 let editButtonWidth : CGFloat = 50 - let editButtonImage = "budgets_disclosure_icon" var cardControllers = [CardInfo]() var bag = DisposeBag() @@ -230,7 +229,7 @@ open class CardsViewController : UIViewController, UICollectionViewDataSource, U }.disposed(by: bag) if getEditModeForIndexPath(indexPath: indexPath) { let editButton = UIButton(frame: CGRect(x: view.bounds.size.width - editButtonOffset - editButtonWidth, y: 0, width: editButtonWidth, height: editButtonHeight)) - editButton.setImage(UIImage(named: editButtonImage, in: Bundle(for: CardsViewController.self), compatibleWith: nil), for: .normal) + editButton.setImage(AssetManager.shared.image(for: .pencil), for: .normal) editButton.addTargetClosure { _ in if let editibalCardTrait = cardController as? EditableCardTrait { editibalCardTrait.onEditButtonTap() diff --git a/Example/Tests/CardPartTitleViewTests.swift b/Example/Tests/CardPartTitleViewTests.swift index 1b1b3a0b..e3fd6b16 100644 --- a/Example/Tests/CardPartTitleViewTests.swift +++ b/Example/Tests/CardPartTitleViewTests.swift @@ -112,18 +112,4 @@ class CardPartTitleViewTests: XCTestCase { XCTAssertEqual(titlePart.menuOptions!, ["111", "222"]) } - func testMenuButtonImageNameProperty() { - - let bag = DisposeBag() - - let titlePart = CardPartTitleView(type: .titleOnly) - - let buttonImageProperty = BehaviorRelay(value: "hello") - buttonImageProperty.asObservable().bind(to: titlePart.rx.menuButtonImageName).disposed(by: bag) - XCTAssertEqual(titlePart.menuButtonImageName, "hello") - - buttonImageProperty.accept("New Value") - XCTAssertEqual(titlePart.menuButtonImageName, "New Value") - } - }