How to make different drawed views out of one UIView Class? - ios

im making a iOS app/game that is like the game "Set".
for that I need to make multiple views so I made this class:
class CardSubview: UIView {
private let partsOfSpace:CGFloat = 12
private let occurenceOfForms: CGFloat = 3
private let color1:UIColor = someColor1
private let color2:UIColor = someColor2
private let color3:UIColor = somecolor3
private let attributeIdentifiers = [1,2,3]
private var openCardUp = false
public var isSelceted = false {
didSet {
if isSelceted == true {
self.backgroundColor = #colorLiteral(red: 0.5791940689, green: 0.1280144453, blue: 0.5726861358, alpha: 0.52734375)
self.layer.borderWidth = 5.0
self.layer.borderColor = #colorLiteral(red: 0.7450980544, green: 0.1568627506, blue: 0.07450980693, alpha: 1)
}
else {
self.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
self.layer.cornerRadius = 0
self.layer.borderWidth = 0
}
}
}
public func makePath() {
openCardUp = true
let path = coloreAndFill(path: self.occurenceOfForm(form: **index1**, occurence: **index2**, viewWidth: self.bounds.width, viewHeigth: self.bounds.height), chosenColor:**index3**, fillIdentifier: **index4**)
path.stroke()
}
override func draw(_ rect: CGRect) {
if openCardUp == true {
makePath()
}
}
....
so all you need to know that this makes an View with an rectangle or triangle or circle etc etc...
now I want to put 80 different of them into the CardBoardView
this is my CardBoardView
import UIKit
#IBDesignable
class CardBoardView: UIView {
static var index1 = 1
static var index2 = 1
static var index3 = 1
static var index4 = 3
public var cells = 12
let values = Values()
var card: CardSubview = CardSubview() {
didSet {
let tabRecognizer = UITapGestureRecognizer(target: self, action: #selector(tab))
card.addGestureRecognizer(tabRecognizer)
}
}
struct Values {
public let ratio:CGFloat = 2.0
public let insetByX:CGFloat = 3.0
public let insetByY:CGFloat = 3.0
public let allAvailableCards = 81
}
#objc private func tab (recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
card.isSelceted = !card.isSelceted
default: break
}
}
override func layoutSubviews() {
super.layoutSubviews()
var grid = Grid(layout: .aspectRatio(values.ratio), frame: self.bounds)
grid.cellCount = 12
for index in 0..<12 {
card = CardSubview(frame: grid[index]!.insetBy(dx: values.insetByX, dy: values.insetByY))
card.makePath()
addSubview(card)
}
}
}
so if I change one of the static indexes the drawing in the CardSubview will change.
thats my idea but it doesn't work because every time I change the index every card will get changed and draws the new form not only one.
how would you do that can anybody give me some thoughts to my code and some tipps?

There's no need for your card property in CardBoardView. You are creating a whole set of CardSubview instances so it makes no sense to have a property that only stores the last one.
You need to make several changes to your code.
Completely remove your card property and its didSet block.
Put the creation of the tap gesture in the loop that creates each CardSubview instance.
Use a local variable in the loop.
Update the tab method to get the card view from the gesture recognizer.
Here's the updated code:
class CardBoardView: UIView {
static var index1 = 1
static var index2 = 1
static var index3 = 1
static var index4 = 3
public var cells = 12
let values = Values()
struct Values {
public let ratio:CGFloat = 2.0
public let insetByX:CGFloat = 3.0
public let insetByY:CGFloat = 3.0
public let allAvailableCards = 81
}
#objc private func tab (recognizer: UITapGestureRecognizer) {
if recognizer.state == .ended {
if let card = recognizer.view as? CardBoardView {
card.isSelected = !card.isSelected
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
var grid = Grid(layout: .aspectRatio(values.ratio), frame: self.bounds)
grid.cellCount = 12
for index in 0..<12 {
let card = CardSubview(frame: grid[index]!.insetBy(dx: values.insetByX, dy: values.insetByY))
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tab))
card.addGestureRecognizer(tapRecognizer)
card.makePath()
addSubview(card)
}
}
}
It's possible that you may need to access each of the CardBoardView instances through some other code you haven't posted. So you may need a property to store all of the cards.
var cards = [CardBoardView]()
Then in the loop that creates each card, add:
cards.append(card)

Related

I am trying to create a custom UITabBar, but there is a memory leak caused when I present one of the tabBar's view controllers

I am trying to create a custom tabBarController, but it seems like there is a memory leak caused by presenting the different view controllers. I can see the memory usage climb when I toggle between different options. I also checked the view hierarchy and noticed that there were a bunch of UITransitionViews. My CustomTabBar is below:
import UIKit
struct CustomTabBarViewControllerObject {
let title: String
let icon: UIImage
let viewController: UIViewController
let index: Int
}
class CustomTabBar: UIView {
// MARK: - Singleton
/// if you plan on using the CustomTabBar singleton then the following should be set:
/// - viewControllerObjects
/// - presentingVC
/// - selectedIconTintColor
/// - unselectedIconTintColor
static let shared = CustomTabBar(frame: .zero)
//MARK: - Variables
private var _viewControllerObjects: [CustomTabBarViewControllerObject]?
public var viewControllerObjects: [CustomTabBarViewControllerObject]? {
get {
return self._viewControllerObjects
} set {
_viewControllerObjects = newValue
if frame.width > 0 {
updateViewControllers()
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
if frame.width > 0 {
updateViewControllers()
}
}
private var viewControllerButtons: [UIButton] = []
private var displayTitles = false
private let iconSize = CGSize(width: 30, height: 30)
public var selectedIconTintColor: UIColor!
public var unselectedIconTintColor: UIColor!
public var presentingVC: UIViewController!
private var selectedIndex = 0
//MARK: - init
private init(_ presentingVC: UIViewController? = nil, frame: CGRect, displayTitles: Bool = false, selectedIconTintColor: UIColor = .text1, unselectedIconTintColor: UIColor = .text3, bgColor: UIColor = .background1) {
self.displayTitles = displayTitles
self.selectedIconTintColor = selectedIconTintColor
self.unselectedIconTintColor = unselectedIconTintColor
self.presentingVC = presentingVC
super.init(frame: frame)
backgroundColor = bgColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Methods
private func updateViewControllers() {
guard let viewControllerObjects = viewControllerObjects else {
return
}
// remove all current icons
for view in subviews {
view.removeFromSuperview()
}
let vcCount = CGFloat(viewControllerObjects.count)
let numberOfSpaces = vcCount+1
let interIconSpacing = (frame.width-(vcCount*iconSize.width))/numberOfSpaces
//let interIconSpacing = (frame.width-(iconSize.width*(CGFloat(vcCount)+1)))/(CGFloat(vcCount)+1)
// app fails if there are too many items in view controller
if interIconSpacing < 5 {fatalError("Too many elements in viewControllerObjects")}
var lastXPosition: CGFloat = 0
let yPosition: CGFloat = frame.height/2-iconSize.height/2
var iconWidthSizeAdjustment: CGFloat = -30
// iterate through viewControllerObjects to add them to the view with icon and action
for vcObject in viewControllerObjects {
iconWidthSizeAdjustment+=30
let vcButton = UIButton(frame: CGRect(origin: CGPoint(x: lastXPosition + interIconSpacing + iconWidthSizeAdjustment, y: yPosition), size: iconSize))
vcButton.setBackgroundImage(vcObject.icon, for: .normal)
vcButton.layoutIfNeeded()
vcButton.subviews.first?.contentMode = .scaleAspectFit
vcButton.addAction(UIAction(title: "", handler: { [unowned self] _ in
if vcObject.index != selectedIndex {
vcObject.viewController.modalPresentationStyle = .fullScreen
self.presentingVC.present(vcObject.viewController, animated: false, completion: nil)
self.presentingVC = vcObject.viewController
self.updateSelected(newSelectedIndex: vcObject.index)
}
}), for: .touchUpInside)
if vcObject.index == selectedIndex {
vcButton.tintColor = selectedIconTintColor
} else {
vcButton.tintColor = unselectedIconTintColor
}
addSubview(vcButton)
viewControllerButtons.append(vcButton)
lastXPosition = lastXPosition + interIconSpacing
}
roundCorners([.topLeft, .topRight], radius: 15)
addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)
}
func updateSelected(newSelectedIndex: Int) {
if viewControllerButtons.indices.contains(selectedIndex) && viewControllerButtons.indices.contains(newSelectedIndex) {
viewControllerButtons[selectedIndex].tintColor = unselectedIconTintColor
viewControllerButtons[newSelectedIndex].tintColor = selectedIconTintColor
selectedIndex = newSelectedIndex
} else {
fatalError("Index does not exist: \(newSelectedIndex)")
}
}
}
This is the CustomTabBarViewController class that all tabBar items inherit from:
import UIKit
class CustomTabBarViewController: UIViewController {
public let customTabBar = CustomTabBar.shared
public let mainView = UIView(frame: .zero)
public var tabBarHeight: CGFloat = 60
override func viewDidLoad() {
super.viewDidLoad()
setUpTabBar()
}
func setUpTabBar() {
// setting up properties of customTabBar
customTabBar.selectedIconTintColor = .text1
customTabBar.unselectedIconTintColor = .text2
customTabBar.presentingVC = self
customTabBar.backgroundColor = .background1
// adding viewControllers for tabBar
customTabBar.viewControllerObjects = [
CustomTabBarViewControllerObject(title: "Home", icon: Images.home, viewController: UINavigationController(rootViewController: HomeViewController()), index: 0),
CustomTabBarViewControllerObject(title: "Search", icon: Images.search, viewController: UINavigationController(rootViewController: SearchViewController()), index: 1)
]
//adding mainView to view
view.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainView.topAnchor.constraint(equalTo: view.topAnchor),
mainView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
mainView.leftAnchor.constraint(equalTo: view.leftAnchor),
mainView.rightAnchor.constraint(equalTo: view.rightAnchor),
])
// add customTabBar to view
view.addSubview(customTabBar)
customTabBar = false
NSLayoutConstraint.activate([
customTabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
customTabBar.leftAnchor.constraint(equalTo: view.leftAnchor),
customTabBar.rightAnchor.constraint(equalTo: view.rightAnchor),
customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight),
])
}
}
So I am going out on a limb here but in
override func layoutSubviews() {
super.layoutSubviews()
if frame.width > 0 {
updateViewControllers()
}
}
I think this is where the problem lies. This function gets called by this system fairly rapidly
private func updateViewControllers() { ...
Combine that with this line
addShadow(shadowColor: UIColor.text1.cgColor, shadowOffset: CGSize(width: 0, height: -1), shadowOpacity: 0.2, shadowRadius: 4)
And basically you have a recipe for disaster. This just keeps adding shadows. There may be other objects getting re added as well but this is what I noticed without full testing and debug. Basically shadows are fairly expensive. They use both a decent amount of computing as well as ram. This will basically re add the shadow every time. I would start by commenting the add shadow and seeing if that reduces resource usage. If it doesn't then comment out the updateViewController() in layoutSubviews().
You can debug using instruments to see what is being allocated causing the spike.
https://www.raywenderlich.com/16126261-instruments-tutorial-with-swift-getting-started

Forwarding events with hitTest() or point(inside, with event) from view added to keyWindow

I've got a small, reusable UIView widget that can be added to any view anywhere, and may or may not always be in the same place or have the same frame. It looks something like this:
class WidgetView: UIView {
// some stuff, not very exciting
}
In my widget view, there's a situation where I need to create a popup menu with an overlay underneath it. it looks like this:
class WidgetView: UIView {
// some stuff, not very exciting
var overlay: UIView!
commonInit() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
overlay = UIView(frame: keyWindow.frame)
overlay.alpha = 0
keyWindow.addSubview(overlay)
// Set some constraints here
someControls = CustomControlsView( ... a smaller controls view ... )
overlay.addSubview(someControls)
// Set some more constraints here!
}
showOverlay() {
overlay.alpha = 1
}
hideOverlay() {
overlay.alpha = 0
}
}
Where this gets complicated, is I'm cutting the shape of the originating WidgetView out of the overlay, so that its controls are still visible underneath. This works fine:
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
... except the problem:
Touches aren't forwarded through the cutout hole. So I thought I'd be clever, and use this extension to determine whether the pixels at the touch point are transparent or not, but I can't even get that far, because hitTest() and point(inside, with event) don't respond to touches outside of the WidgetView's frame.
The way I can see it, there are four (potential) ways to solve this, but I can't get any of them working.
Find some magical (🦄) way to to make hitTest or point(inside) respond anywhere in the keyWindow, or at least the overlayView's frame
Add a UITapGestureRecognizer to the overlayView and forward the appropriate touches to the originating view controller (this partially works — the tap gesture responds, but I don't know where to go from there)
Use a delegate/protocol implementation to tell the original WidgetView to respond to touches
Add the overlay and its subviews to a different parent view altogether that isn't the keyWindow?
Below the fold, here is a complete executable setup, which relies on a new single view project with storyboard. It relies on SnapKit constraints, for which you can use the following podfile:
podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'YourTarget' do
pod 'SnapKit', '~> 4.2.0'
end
ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController {
public var utilityToolbar: UtilityToolbar!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
setup()
}
func setup() {
let button1 = UtilityToolbar.Button(title: "One", buttonPressed: nil)
let button2 = UtilityToolbar.Button(title: "Two", buttonPressed: nil)
let button3 = UtilityToolbar.Button(title: "Three", buttonPressed: nil)
let button4 = UtilityToolbar.Button(title: "Four", buttonPressed: nil)
let button5 = UtilityToolbar.Button(title: "Five", buttonPressed: nil)
let menuItems: [UtilityToolbar.Button] = [button1, button2, button3, button4, button5]
menuItems.forEach({
$0.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
})
utilityToolbar = UtilityToolbar(title: "One", menuItems: menuItems)
utilityToolbar.titleButton.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
utilityToolbar.backgroundColor = .white
utilityToolbar.dropdownContainer.backgroundColor = .white
view.addSubview(utilityToolbar)
utilityToolbar.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(250)
make.height.equalTo(50.0)
}
}
}
CutoutView.swift
import UIKit
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else { return }
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
UtilityToolbar.swift
import Foundation import UIKit import SnapKit
class UtilityToolbar: UIView {
class Button: UIButton {
var functionIdentifier: String?
var buttonPressed: (() -> Void)?
fileprivate var owner: UtilityToolbar?
convenience init(title: String, buttonPressed: (() -> Void)?) {
self.init(type: .custom)
self.setTitle(title, for: .normal)
self.functionIdentifier = title.lowercased()
self.buttonPressed = buttonPressed
}
}
enum MenuState {
case open
case closed
}
enum TitleStyle {
case label
case dropdown
}
private(set) public var menuState: MenuState = .closed
var itemHeight: CGFloat = 50.0
var spacing: CGFloat = 6.0 { didSet { dropdownStackView.spacing = spacing } }
var duration: TimeInterval = 0.15
var dropdownContainer: UIView!
var titleButton: UIButton = UIButton()
#IBOutlet weak fileprivate var toolbarStackView: UIStackView!
private var stackViewBottomConstraint: Constraint!
private var dropdownStackView: UIStackView!
private var overlayView: CutoutView!
private var menuItems: [Button] = []
private var expandedHeight: CGFloat { get { return CGFloat(menuItems.count - 1) * itemHeight + (spacing * 2) } }
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience init(title: String, menuItems: [Button]) {
self.init()
self.titleButton.setTitle(title, for: .normal)
self.menuItems = menuItems
commonInit()
}
private func commonInit() {
self.addSubview(titleButton)
titleButton.addTarget(self, action: #selector(titleButtonPressed(_:)), for: .touchUpInside)
titleButton.snp.makeConstraints { $0.edges.equalToSuperview() }
dropdownContainer = UIView()
dropdownStackView = UIStackView()
dropdownStackView.axis = .vertical
dropdownStackView.distribution = .fillEqually
dropdownStackView.alignment = .fill
dropdownStackView.spacing = spacing
dropdownStackView.alpha = 0
dropdownStackView.translatesAutoresizingMaskIntoConstraints = true
menuItems.forEach({
$0.owner = self
$0.addTarget(self, action: #selector(menuButtonPressed(_:)), for: .touchUpInside)
})
}
override func layoutSubviews() {
super.layoutSubviews()
// Block if the view isn't fully ready, or if the containerView has already been added to the window
guard
let keyWindow = UIApplication.shared.keyWindow,
self.globalFrame != .zero,
dropdownContainer.superview == nil else { return }
overlayView = CutoutView(frame: keyWindow.frame)
overlayView.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5)
overlayView.alpha = 0
overlayView.holes = [self.globalFrame!]
keyWindow.addSubview(overlayView)
keyWindow.addSubview(dropdownContainer)
dropdownContainer.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset((self.globalFrame?.origin.y ?? 0) + self.frame.height)
make.height.equalTo(0)
}
dropdownContainer.addSubview(dropdownStackView)
dropdownStackView.snp.makeConstraints({ (make) in
make.left.right.equalToSuperview().inset(spacing).priority(.required)
make.top.equalToSuperview().priority(.medium)
stackViewBottomConstraint = make.bottom.equalToSuperview().priority(.medium).constraint
})
}
public func openMenu() {
titleButton.isSelected = true
dropdownStackView.addArrangedSubviews(menuItems.filter { $0.titleLabel?.text != titleButton.titleLabel?.text })
dropdownContainer.layoutIfNeeded()
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(self.expandedHeight)
}
stackViewBottomConstraint.update(inset: spacing)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 1
self.dropdownStackView.alpha = 1
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .open
}
}
public func closeMenu() {
titleButton.isSelected = false
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(0)
}
stackViewBottomConstraint.update(inset: 0)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 0
self.dropdownStackView.alpha = 0
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .closed
self.dropdownStackView.removeAllArrangedSubviews()
}
}
#objc private func titleButtonPressed(_ sender: Button) {
switch menuState {
case .open:
closeMenu()
case .closed:
openMenu()
}
}
#objc private func menuButtonPressed(_ sender: Button) {
closeMenu()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Nothing of interest is happening here unless the touch is inside the containerView
print(UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0)
if UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0 {
return true
}
return super.point(inside: point, with: event)
} }
Extensions.swift
import UIKit
extension UIWindow {
static var topController: UIViewController? {
get {
guard var topController = UIApplication.shared.keyWindow?.rootViewController else { return nil }
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
}
public extension UIView {
var globalPoint: CGPoint? {
return self.superview?.convert(self.frame.origin, to: nil)
}
var globalFrame: CGRect? {
return self.superview?.convert(self.frame, to: nil)
}
}
extension UIColor {
static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {
var pixel: [CUnsignedChar] = [0, 0, 0, 0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
context!.translateBy(x: -point.x, y: -point.y)
view.layer.render(in: context!)
let red: CGFloat = CGFloat(pixel[0]) / 255.0
let green: CGFloat = CGFloat(pixel[1]) / 255.0
let blue: CGFloat = CGFloat(pixel[2]) / 255.0
let alpha: CGFloat = CGFloat(pixel[3]) / 255.0
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
}
extension UIStackView {
func addArrangedSubviews(_ views: [UIView?]) {
views.filter({$0 != nil}).forEach({ self.addArrangedSubview($0!)})
}
func removeAllArrangedSubviews() {
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
self.removeArrangedSubview(subview)
return allSubviews + [subview]
}
// Deactivate all constraints
NSLayoutConstraint.deactivate(removedSubviews.flatMap({ $0.constraints }))
// Remove the views from self
removedSubviews.forEach({ $0.removeFromSuperview() })
}
}
Silly me, I need to put the hitTest on the overlay view (CutoutView) not the calling view.
class CutoutView: UIView {
// ...
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
return super.hitTest(point, with: event)
}
}

Swift 4: didSet #IBOutlet calls viewDidLayoutSubviews, but not with addSubview()

When selectCard is called, by commenting out various parts of the code I've discovered that viewDidLayoutSubviews is only called when score is updated in checkIfCardsMatch function. The score is only updated if we have matchedCards. Why is this happening?
I would like to have viewDidLayoutSubviews called every time selectCard is called. I noticed if I add CardTable.addSubview(grid) to my resetTable function than viewDidLayoutSubviews is called every time selectCard is called EXECPT when score is changed.
What is going on here and how can I make sure viewDidLayoutSubviews is called on each card selection?
// Score
#IBOutlet weak var scoreLabel: UILabel! {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
private var score = 0 {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
func selectCard(card: Card) {
// must deal more cards if we have a match first
if !matched.isEmpty && !game.cards.isEmpty {
print("must deal more cards if we have a match first")
resetTable()
return
}
// deal no longer possible
if !matched.isEmpty && game.cards.isEmpty {
print("deal no longer possible")
clearAndDeal()
}
// reset any mismatched card styles
if !misMatched.isEmpty {
print("reset any mismatched card styles")
resetMisMatchedStyle()
}
// select or deselect card
game.select(card: card)
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .selected
}
// check for match
checkIfCardsMatch()
// resetTable
resetTable()
}
private func resetTable(){
grid = CardTableView(frame: CardTable.bounds, cardsInPlay: visibleCards)
grid.delegate = self
}
private func checkIfCardsMatch(){
if let matchedCards = game.matchedCards() {
print("MATCHED!", matchedCards)
matched = matchedCards
game.clearSelectedCards()
score += 3
// set visible cards to matched
for card in matched {
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .matched
}
}
}else {
if game.selectedCards.count == 3 {
print("no match")
misMatched = game.selectedCards
game.clearSelectedCards()
score -= 5
for card in misMatched {
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .misMatched
}
}
}
}
}
The whole file:
//
// ViewController.swift
// Set
//
import UIKit
class ViewController: UIViewController, CardTableViewDelegate {
func delegateCardTap(card: Card){
print("called in view controller")
selectCard(card: card)
}
// Game
private var game = SetGame()
private lazy var grid = CardTableView(frame: CardTable.bounds, cardsInPlay: visibleCards)
// table to place all cards
#IBOutlet weak var CardTable: UIView! {
didSet {
// set up buttons with 12 cards
initalDeal()
}
}
#IBAction func newGame(_ sender: UIButton) {
score = 0
game = SetGame()
visibleCards.removeAll()
matched.removeAll()
misMatched.removeAll()
dealMoreButton.isEnabled = true
dealMoreButton.setTitleColor(#colorLiteral(red: 0.231372549, green: 0.6, blue: 0.9882352941, alpha: 1), for: .normal)
initalDeal()
grid.cards = visibleCards
}
override func viewDidLoad() {
grid.delegate = self
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
super.viewDidLayoutSubviews()
// reset frame when device rotates
grid.frame = CardTable.bounds
// add cards to the card table
CardTable.addSubview(grid)
}
// Cards
private var visibleCards = [Card]()
// Score
#IBOutlet weak var scoreLabel: UILabel! {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
private var score = 0 {
didSet {
scoreLabel.text = "Score: \(score)"
}
}
// Deal
#IBOutlet weak var dealMoreButton: UIButton!
#IBAction func deal(_ sender: UIButton) {
// have a match and cards available to deal
if !matched.isEmpty && !game.cards.isEmpty {
//TODO: fix this for new UI
clearAndDeal()
} else {
dealThreeMore()
}
// disable if we run out of cards
if game.cards.isEmpty {
disable(button: sender)
}
grid.cards = visibleCards
}
private func dealThreeMore(){
if visibleCards.count < game.cardTotal {
for _ in 0..<3 {
if let card = game.drawCard() {
// add more visible cards
visibleCards.append(card)
} else {
print("ran out of cards in the deck!")
}
}
}
}
private func disable(button sender: UIButton){
sender.isEnabled = false
sender.setTitleColor(#colorLiteral(red: 0.5, green: 0.5, blue: 0.5, alpha: 1), for: .normal)
}
private func clearAndDeal(){
print("in clearAndDeal")
//TODO: rewrite me
for card in matched {
if let index = visibleCards.index(of: card){
// remove matched styles
// draw new card
if let newCard = game.drawCard() {
// swap with old card
replace(old: index, with: newCard)
} else {
// ran out of cards in the deck!
hideButton(by: index)
}
}
}
matched.removeAll()
}
private var allCardsMatched: Bool {
let cards = visibleCards.filter({card in
// if let index = visibleCards.index(of: card){
//// return cardButtons[index].isEnabled
// }
return false
})
return cards.count == 3
}
private var misMatched = [Card]()
private var matched = [Card]()
func selectCard(card: Card) {
// must deal more cards if we have a match first
if !matched.isEmpty && !game.cards.isEmpty {
print("must deal more cards if we have a match first")
resetTable()
return
}
// deal no longer possible
if !matched.isEmpty && game.cards.isEmpty {
print("deal no longer possible")
clearAndDeal()
}
// reset any mismatched card styles
if !misMatched.isEmpty {
print("reset any mismatched card styles")
resetMisMatchedStyle()
}
// select or deselect card
game.select(card: card)
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .selected
}
// check for match
checkIfCardsMatch()
// resetTable
resetTable()
}
private func resetTable(){
grid = CardTableView(frame: CardTable.bounds, cardsInPlay: visibleCards)
grid.delegate = self
}
private func checkIfCardsMatch(){
if let matchedCards = game.matchedCards() {
print("MATCHED!", matchedCards)
matched = matchedCards
game.clearSelectedCards()
score += 3
// set visible cards to matched
for card in matched {
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .matched
}
}
}else {
if game.selectedCards.count == 3 {
print("no match")
misMatched = game.selectedCards
game.clearSelectedCards()
score -= 5
for card in misMatched {
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = .misMatched
}
}
}
}
}
private func initalDeal(){
for _ in 0..<12 {
if let card = game.drawCard() {
visibleCards.append(card)
}
}
}
private func removeStyleFrom(button: UIButton){
button.layer.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
}
private func resetMisMatchedStyle(){
for card in misMatched {
if let idx = visibleCards.index(of: card){
visibleCards[idx].state = nil
}
}
misMatched.removeAll()
}
private func replace(old index: Int, with newCard: Card){
visibleCards[index] = newCard
// style(a: cardButtons[index], by: newCard)
}
private func hideButton(by index: Int){
// let button = cardButtons[index]
// button.setAttributedTitle(NSAttributedString(string:""), for: .normal)
// button.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0)
// button.isEnabled = false
}
private func styleTouched(button: UIButton, by card: Card) {
if game.selectedCards.contains(card) {
button.layer.backgroundColor = #colorLiteral(red: 0.9848672538, green: 0.75109528, blue: 1, alpha: 1)
}else {
removeStyleFrom(button: button)
}
}
}

Make circular photos downloaded from google (google places, swift)

I'm working with google places and i have a VC with a tableView where i downloaded nearby places from the user position and in each cell of the tableView i add the information of the place and the photo of it. I created a custom class to make the photo circular
import UIKit
#IBDesignable
class RoundImage: UIImageView {
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
self.layer.cornerRadius = cornerRadius
}
}
#IBInspectable var borderWidth: CGFloat = 0 {
didSet {
self.layer.borderWidth = borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear {
didSet {
self.layer.borderColor = borderColor.cgColor
}
}
}
but the problem is that i download the photos of the places with this class
import UIKit
private let widthKey = "width"
private let heightKey = "height"
private let photoReferenceKey = "photo_reference"
class QPhoto: NSObject {
var width: Int?
var height: Int?
var photoRef: String?
init(photoInfo: [String:Any]) {
height = photoInfo[heightKey] as? Int
width = photoInfo[widthKey] as? Int
photoRef = photoInfo[photoReferenceKey] as? String
}
func getPhotoURL(maxWidth:Int) -> URL? {
if let ref = self.photoRef {
return NearbyPlaces.googlePhotoURL(photoReference: ref, maxWidth: maxWidth)
}
return nil
}
}
and i would like to know how can i adjust it, because with this class the photos that i download even though i put the RoundImage class at the imageview in the storyboard are always square
You need to set clipsToBounds to true. You can do that when you set the cornerRadius:
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
self.layer.cornerRadius = cornerRadius
self.clipsToBounds = true
}
}

Change stackView nested label color programatically when separate image is tapped. Swift

I have an array of "hotspot views" which are just UIImage's and an array of stackviews which contain two labels in each. Im trying to get the color of the hotspot view image and one of the labels to change to red when the hotspot view is tapped. I cant seem to find out how after googling most of the day. Any insight would be great.
Here is my code below:
I have commented out in the tap gesture function what i was hoping to achieve but i have no idea how to access the nested labels in the stackview or if im using the tap gesture recognizer correctly.
import UIKit
import OAStackView
protocol StandMapHotspotLayerViewDataSource {
func numberOfHotspots(standMapHotspotLayerView: StandMapHotspotLayerView) -> Int
func hotspotViewForIndex(index: Int, inStandMapHotspotLayerView: StandMapHotspotLayerView) -> (UIView, OAStackView)
}
struct HotspotDataSource {
var stackView: [OAStackView] = []
var hotspotView: [UIView] = []
}
class StandMapHotspotLayerView: UIView {
var dataSource: StandMapHotspotLayerViewDataSource?
var hotspotDataSource = HotspotDataSource()
override func layoutSubviews() {
super.layoutSubviews()
let hotspotCount = self.dataSource?.numberOfHotspots(self) ?? 0
(0..<hotspotCount).map({ index in
return self.dataSource!.hotspotViewForIndex(index, inStandMapHotspotLayerView: self)
}).forEach({ hotspotView, stackView in
hotspotDataSource.hotspotView.append(hotspotView)
hotspotDataSource.stackView.append(stackView)
hotspotView.userInteractionEnabled = true
let gesture = UITapGestureRecognizer(target: hotspotView, action: #selector(self.hotspotWasPressed(_:)))
self.addGestureRecognizer(gesture)
self.addSubview(hotspotView)
self.addSubview(stackView)
})
addLine()
}
func hotspotWasPressed(sender: UITapGestureRecognizer) {
//
// sender.numberOfTouchesRequired = 1
//
// let hotspotView = hotspotDataSource.hotspotView[index]
// let stackView = hotspotDataSource.stackView[index]
//
// hotspotView.tintColor = UIColor(red: 157, green: 27, blue: 50, alpha: 1)
// stackView
}
func addLine() {
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for index in 0..<self.dataSource!.numberOfHotspots(self) {
let stackView = hotspotDataSource.stackView[index]
let hotspotView = hotspotDataSource.hotspotView[index]
if stackView.frame.origin.y < 100 {
let stackViewPoint = CGPointMake(stackView.frame.origin.x + stackView.frame.size.width / 2, stackView.frame.origin.y + stackView.frame.size.height)
let imageViewPoint = CGPointMake((hotspotView.frame.origin.x + hotspotView.frame.size.width / 2), hotspotView.frame.origin.y)
path.moveToPoint(stackViewPoint)
path.addLineToPoint(imageViewPoint)
} else {
let stackViewPoint = CGPointMake(stackView.frame.origin.x + stackView.frame.size.width / 2, stackView.frame.origin.y)
let imageViewPoint = CGPointMake((hotspotView.frame.origin.x + hotspotView.frame.size.width / 2), hotspotView.frame.origin.y + hotspotView.bounds.size.height)
path.moveToPoint(stackViewPoint)
path.addLineToPoint(imageViewPoint)
}
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.lineWidth = 0.2
shapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(shapeLayer)
}
}
func reloadData() {
self.setNeedsLayout()
}
}
Thanks for any help in advance.
Figured it out. See code below:
import UIKit
import OAStackView
protocol StandMapHotspotLayerViewDataSource {
func numberOfHotspots(standMapHotspotLayerView: StandMapHotspotLayerView) -> Int
func hotspotViewForIndex(index: Int, inStandMapHotspotLayerView: StandMapHotspotLayerView) -> (UIImageView, OAStackView)
}
struct HotspotViews {
var stackView: [OAStackView] = []
var hotspotView: [UIImageView] = []
}
class StandMapHotspotLayerView: UIView {
var dataSource: StandMapHotspotLayerViewDataSource?
var hotspotViews = HotspotViews()
override func layoutSubviews() {
super.layoutSubviews()
let hotspotCount = self.dataSource?.numberOfHotspots(self) ?? 0
var i: Int = 0
(0..<hotspotCount).map({ index in
return self.dataSource!.hotspotViewForIndex(index, inStandMapHotspotLayerView: self)
}).forEach({ hotspotView, stackView in
hotspotViews.hotspotView.append(hotspotView)
hotspotViews.stackView.append(stackView)
hotspotView.userInteractionEnabled = true
hotspotView.tag = i
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.hotspotWasPressed(_:)))
hotspotView.addGestureRecognizer(gesture)
self.addSubview(hotspotView)
self.addSubview(stackView)
i += 1
})
addLine()
}
func hotspotWasPressed(sender: UITapGestureRecognizer) {
let index = sender.view!.tag
let hotspot = hotspotViews.hotspotView[index]
let textLabel = hotspotViews.stackView[index].arrangedSubviews.first as? UILabel
hotspot.image = UIImage(named: "RedHotspotImage")
textLabel?.textColor = UIColor(red: 158/255, green: 27/255, blue: 50/255, alpha: 1)
//go to hotspot url: StandMapView.hotspots[index].url
}
func addLine() {
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for index in 0..<self.dataSource!.numberOfHotspots(self) {
let stackView = hotspotViews.stackView[index]
let hotspotView = hotspotViews.hotspotView[index]
if stackView.frame.origin.y < 100 {
let stackViewPoint = CGPointMake(stackView.frame.origin.x + stackView.frame.size.width / 2, stackView.frame.origin.y + stackView.frame.size.height)
let imageViewPoint = CGPointMake((hotspotView.frame.origin.x + hotspotView.frame.size.width / 2), hotspotView.frame.origin.y)
path.moveToPoint(stackViewPoint)
path.addLineToPoint(imageViewPoint)
} else {
let stackViewPoint = CGPointMake(stackView.frame.origin.x + stackView.frame.size.width / 2, stackView.frame.origin.y)
let imageViewPoint = CGPointMake((hotspotView.frame.origin.x + hotspotView.frame.size.width / 2), hotspotView.frame.origin.y + hotspotView.bounds.size.height)
path.moveToPoint(stackViewPoint)
path.addLineToPoint(imageViewPoint)
}
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.lineWidth = 0.2
shapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(shapeLayer)
}
}
func reloadData() {
self.setNeedsLayout()
}
}
Important parts were to pass a .tag through on the hotspot view and then to access the label within the stackview by
stackView[index].arrangedSubviews.first as? UILabel
thanks

Resources