my app part is based on canvas.
#What I want to do?
I need dictionary to track and save particular player's key and value.because, I want to store player1 on key "1" and player2 on key "2", suppose user change player 2's number, then player 2's number change not key "2".
I can place multiple players (cashapelayers - with text), on imageview. and can edit text label on double click for same player.
my issue is, whenever i click second time on same player, it shows wrong number, as well when click okay it creates new player. what i want is to edit a player's label(number) with different number, and don't create a new one till I click on imageview.
I have try to create playerview in touchedEnded but I'm fail, also try to search for the same issue on other resources.
I have added some images for reference.
class AddPlayerStruct {
var addPlayerViewStruct : AddPlayerView?
var addPlayerViewsArrStruct : [AddPlayerView] = []
var Label = UILabel()
}
import UIKit
class ViewController: UIViewController {
//MARK: AddPlayerView Variables
var addPlayerView : AddPlayerView?
var addPlayerViews: [AddPlayerView] = []
var draggedAddPlayer: AddPlayerView?
let addPlayerWidth : CGFloat = 40
var addPlayerDict : [String : AddPlayerStruct] = [:]
var playerCount : Int = 1
var label = UILabel()
var isDobuleClick : Bool = false
@IBOutlet weak var images: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedAddPlayer = draggedAddPlayer, let point = touches.first?.location(in: images) else {
return
}
draggedAddPlayer.center = point
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a circle is being dragged
// or if we do not have a coordinate
guard draggedAddPlayer == nil, let point = touches.first?.location(in: images) else {
return
}
// Do not create new circle if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
if let draggedAddPlayer = addPlayerViews.filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedAddPlayer = draggedAddPlayer
return
}
// Create new circle and store in dict
let rect = CGRect(x: point.x - 20, y: point.y - 20, width: addPlayerWidth, height: addPlayerWidth)
addPlayerView = AddPlayerView(frame: rect)
addPlayerView?.backgroundColor = .white
addPlayerView?.isUserInteractionEnabled = true
addPlayerView?.image = UIImage(named: "player")
addPlayerView?.tintColor = .systemBlue
addPlayerViews.append(addPlayerView!)
images.addSubview(addPlayerView!)
// The newly created view can be immediately dragged
draggedAddPlayer = addPlayerView
//Add Player label as Number
// playerCount = addPlayerDict.count + 1
var addPlayerStruct = AddPlayerStruct()
label = UILabel(frame: CGRect(x: rect.width / 2 - 8, y: rect.height / 2 + 5, width: 16, height: 10))
if addPlayerStruct.Label.text == nil{
label.text = String(addPlayerDict.count + 1)
}
label.font = UIFont(name: "Helvetica" , size: 10)
label.textColor = UIColor.white
label.textAlignment = NSTextAlignment.center
label.isUserInteractionEnabled = true
addPlayerView!.addSubview(label)
debugPrint(addPlayerDict)
addPlayerStruct.addPlayerViewStruct = addPlayerView
addPlayerStruct.addPlayerViewsArrStruct.append(contentsOf: addPlayerViews)
addPlayerDict.updateValue(addPlayerStruct, forKey: String(addPlayerDict.count + 1))
debugPrint(addPlayerDict)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
draggedAddPlayer = nil
var selectedPlayerKey = String()
var addPlayerStruct = AddPlayerStruct()
var selectedPoint = CGPoint()
if isDobuleClick == true {
guard let point = touches.first?.location(in: images) else {
return
}
//TODO: check which dict key's playerview has same point
addPlayerDict.forEach { (key,value) in
debugPrint(key)
debugPrint(value)
// debugPrint("before \(addPlayerDict.count)")
//TODO: get selected dict key and get all data of it
guard let points = value.addPlayerViewStruct?.frame.contains(point) else { return }
if points{
selectedPlayerKey = key
selectedPoint = point
addPlayerViews.append(contentsOf: value.addPlayerViewsArrStruct)
debugPrint(addPlayerViews.last)
addPlayerView = value.addPlayerViewStruct //selected playerview
debugPrint(selectedPlayerKey)
// show data on alertview textfield
let alert = UIAlertController(title: "Player Number", message: "Enter new player Number", preferredStyle: .alert)
alert.addTextField { textData in
if addPlayerStruct.Label.text == nil{
textData.text = selectedPlayerKey
}else{
textData.text = addPlayerStruct.Label.text
}
}
// get changed data from textfield and save back to same dict key's playerview - Don't change key.
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
let textfield = alert.textFields?[0]
addPlayerStruct.Label.text = textfield?.text!
// let index = self.addPlayerDict.index(forKey: selectedPlayerKey)
addPlayerStruct.addPlayerViewStruct = self.addPlayerView
self.draggedAddPlayer = self.addPlayerView
self.addPlayerViews.removeLast()
self.addPlayerView?.removeFromSuperview() //selected playerview removed from iamgeview
//
// // Show on playerview
//
let rect = CGRect(x: selectedPoint.x - 20, y: selectedPoint.y - 20, width: self.addPlayerWidth, height: self.addPlayerWidth)
self.addPlayerView = AddPlayerView(frame: rect)
self.addPlayerView?.backgroundColor = .white
self.addPlayerView?.isUserInteractionEnabled = true
self.addPlayerView?.image = UIImage(named: "player")
self.addPlayerView?.tintColor = .systemBlue
self.addPlayerViews.append(self.addPlayerView!)
self.images.addSubview(self.addPlayerView!)
self.label = UILabel(frame: CGRect(x: rect.width / 2 - 8, y: rect.height / 2 + 5, width: 16, height: 10))
self.label.text = textfield?.text!
self.label.font = UIFont(name: "Helvetica" , size: 10)
self.label.textColor = UIColor.white
self.label.textAlignment = NSTextAlignment.center
self.label.isUserInteractionEnabled = true
self.addPlayerView!.addSubview(self.label)
debugPrint(self.addPlayerDict.count)
self.addPlayerDict.updateValue(addPlayerStruct, forKey: selectedPlayerKey)
debugPrint(addPlayerStruct.addPlayerViewStruct?.frame)
debugPrint(self.addPlayerDict.count)
}))
self.present(alert, animated: false)
}
}
}
isDobuleClick = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.isDobuleClick = false
}
}
}
#AddPlayerView
class AddPlayerView : UIImageView{
var shapeLayer = CAShapeLayer()
var addPlayerPath = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){
self.backgroundColor = .clear
// let shapeLayer = CAShapeLayer()
addPlayerPath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2, y: frame.size.height / 2), radius: 20, startAngle: 0, endAngle: 360, clockwise: true)
shapeLayer.path = addPlayerPath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(shapeLayer)
}
}
You have a decent approach, but a couple tips...
Your class and var naming is rather confusing. For example, instead of naming your image view images
it makes much more sense to name it something like playingFieldImageView
. And, instead of AddPlayerView
(which sounds like an action), just name it PlayerView
.
You don't need to keep a separate array of player views or a dictionary of player structs... When you add a PlayerView
as a subview of playingFieldImageView
, you can track it with the .subviews
property of playingFieldImageView
.
You can move a lot of the logic you're using to manage the player view into the player view class... along these lines:
class PlayerView : UIImageView {
public var playerNumber: Int = 0 {
didSet {
playerLabel.text = "\(playerNumber)"
}
}
private let shapeLayer = CAShapeLayer()
private let playerLabel: UILabel = {
let v = UILabel()
v.font = UIFont(name: "Helvetica" , size: 10)
v.textColor = .white
v.textAlignment = NSTextAlignment.center
v.text = "0"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
self.backgroundColor = .white
self.tintColor = .systemBlue
if let img = UIImage(named: "player") {
self.image = img
} else {
if let img = UIImage(systemName: "person.fill") {
self.image = img
} else {
self.backgroundColor = .green
}
}
// if using as a mask, can be any opaque color
shapeLayer.fillColor = UIColor.black.cgColor
// assuming you want a "round" view
//self.layer.addSublayer(shapeLayer)
self.layer.mask = shapeLayer
// add and constrain the label as a subview
addSubview(playerLabel)
NSLayoutConstraint.activate([
playerLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
playerLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
playerLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
])
}
override func layoutSubviews() {
// update the mask path here (we have accurate bounds)
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
Now you can add a new PlayerView
and assign its .playerNumber
... instead of all of the set image, add label, etc. And, when the user wants to change the "player number" you can update the .playerNumber
property instead of removing / re-creating views.
When trying to track touches and taps (particularly double-taps), it is much easier to add a double-tap gesture recognizer to the subview itself.
Here's that same PlayerView
class, but with a gesture recognizer -- and a closure to tell the controller that it was double-tapped:
class PlayerView : UIImageView {
// closure to tell the controller this view was double-tapped
public var gotDoubleTap: ((PlayerView) -> ())?
public var playerNumber: Int = 0 {
didSet {
playerLabel.text = "\(playerNumber)"
}
}
private let shapeLayer = CAShapeLayer()
private let playerLabel: UILabel = {
let v = UILabel()
v.font = UIFont(name: "Helvetica" , size: 10)
v.textColor = .white
v.textAlignment = NSTextAlignment.center
v.text = "0"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
self.backgroundColor = .white
self.tintColor = .systemBlue
if let img = UIImage(named: "player") {
self.image = img
} else {
if let img = UIImage(systemName: "person.fill") {
self.image = img
} else {
self.backgroundColor = .green
}
}
// if using as a mask, can be any opaque color
shapeLayer.fillColor = UIColor.black.cgColor
// assuming you want a "round" view
//self.layer.addSublayer(shapeLayer)
self.layer.mask = shapeLayer
// add and constrain the label as a subview
addSubview(playerLabel)
NSLayoutConstraint.activate([
playerLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
playerLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
playerLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
])
// add a double-tap gesture recognizer
let g = UITapGestureRecognizer(target: self, action: #selector(doubleTapHandler(_:)))
g.numberOfTapsRequired = 2
addGestureRecognizer(g)
}
override func layoutSubviews() {
// update the mask path here (we have accurate bounds)
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
@objc func doubleTapHandler(_ g: UITapGestureRecognizer) {
// tell the controller we were double-tapped
gotDoubleTap?(self)
}
}
You can now simplify all of your touch code to handle only adding new or moving the player views.
Here's an example controller class, using the above PlayerView
class:
class AddPlayerViewController: UIViewController {
//MARK: AddPlayerView Variables
// we can declare this as a standard UIView
var draggedPlayerView: UIView?
let playerViewWidth : CGFloat = 40
@IBOutlet var playingFieldImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
playingFieldImageView.isUserInteractionEnabled = true
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedAddPlayer = draggedPlayerView, let point = touches.first?.location(in: playingFieldImageView) else {
return
}
draggedAddPlayer.center = point
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a PlayerView is being dragged
// or if we do not have a coordinate
guard draggedPlayerView == nil, let point = touches.first?.location(in: playingFieldImageView) else {
return
}
// Do not create new PlayerView if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
// filter only subviews which are PlayerView class (in case we've added another subview type)
if let draggedAddPlayer = playingFieldImageView.subviews.filter({ $0 is PlayerView }).filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedPlayerView = draggedAddPlayer
return
}
// Create new PlayerView
let rect = CGRect(x: point.x - 20, y: point.y - 20, width: playerViewWidth, height: playerViewWidth)
let newPlayerView = PlayerView(frame: rect)
// give the new player the lowest available number
// for example, if we've created numbers:
// "1" "2" "3" "4" "5"
// we want to assign "6" to the new guy
// but... if the user has changed the 3rd player to "12"
// we'll have players:
// "1" "2" "12" "4" "5"
// so we want to assign "3" to the new guy
let nums = playingFieldImageView.subviews.compactMap{$0 as? PlayerView}.compactMap { $0.playerNumber }
var newNum: Int = 1
for i in 1..<nums.count + 2 {
if !nums.contains(i) {
newNum = i
break
}
}
newPlayerView.playerNumber = newNum
// set the double-tap closure
newPlayerView.gotDoubleTap = { [weak self] pv in
guard let self = self else { return }
self.changePlayerNumber(pv)
}
playingFieldImageView.addSubview(newPlayerView)
// The newly created view can be immediately dragged
draggedPlayerView = newPlayerView
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
draggedPlayerView = nil
}
// called when a PlayerView is double-tapped
func changePlayerNumber(_ playerView: PlayerView) {
// show data on alertview textfield
let alert = UIAlertController(title: "Player Number", message: "Enter new player Number", preferredStyle: .alert)
alert.addTextField { textData in
textData.text = "\(playerView.playerNumber)"
}
// get changed data from textfield and update the playerNumber for the PlayerView
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
guard let tf = alert.textFields?.first,
let newNumString = tf.text,
let newNumInt = Int(newNumString),
newNumInt != playerView.playerNumber
else {
// user entered something that was not a number, or
// tapped OK without changing the number
return
}
// don't allow a duplicate player number
let nums = self.playingFieldImageView.subviews.compactMap{$0 as? PlayerView}.compactMap { $0.playerNumber }
if nums.contains(newNumInt) {
let dupAlert = UIAlertController(title: "Duplicate PLayer Number", message: "Somebody else already has number: \(newNumInt)", preferredStyle: .alert)
dupAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
self.changePlayerNumber(playerView)
}))
self.present(dupAlert, animated: true)
} else {
playerView.playerNumber = newNumInt
}
}))
self.present(alert, animated: false)
}
}