This is the code of creating a custom segmented control i found over the Internet. I have a problem in understanding the last two functions, beginTrackingWithTouch and layoutSubviews. What are the purpose of these functions and what does their code do exactly
and finally, excuse this question. I'm still a beginner in iOS development and I'm just seeking help.
#IBDesignable class CustomSegmentedControl : UIControl {
private var labels = [UILabel]()
var items = ["Item 1","Item 2"] {
didSet {
setUpLabels()
}
}
var thumbView = UIView()
var selectedIndex : Int = 0 {
didSet {
displayNewSelectedIndex()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView() {
//layer.cornerRadius = frame.height / 2
layer.borderColor = UIColor.blackColor().CGColor
layer.borderWidth = 2
backgroundColor = UIColor.clearColor()
setUpLabels()
insertSubview(thumbView, atIndex: 0)
}
func setUpLabels() {
for label in labels {
label.removeFromSuperview()
}
labels.removeAll(keepCapacity: true)
for index in 1...items.count {
let label = UILabel(frame: CGRectZero)
label.text = items[index-1]
label.textAlignment = .Center
label.textColor = UIColor.blackColor()
self.addSubview(label)
labels.append(label)
}
}
func displayNewSelectedIndex() {
let label = labels[selectedIndex]
self.thumbView.frame = label.frame
}
override func layoutSubviews() {
super.layoutSubviews()
var selectFrame = self.bounds
let newWidth = CGRectGetWidth(selectFrame) / CGFloat(items.count)
selectFrame.size.width = newWidth
thumbView.frame = selectFrame
thumbView.backgroundColor = UIColor.grayColor()
//thumbView.layer.cornerRadius = thumbView.frame.height / 2
let labelHeight = self.bounds.height
let labelWidth = self.bounds.width / CGFloat(labels.count)
for index in 0..<labels.count {
let label = labels[index]
let xPosition = CGFloat(index) * labelWidth
label.frame = CGRectMake(xPosition, 0, labelWidth, labelHeight)
}
}
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let location = touch.locationInView(self)
var calculatedIndex : Int?
for (index, item) in labels.enumerate() {
if item.frame.contains(location) {
calculatedIndex = index
}
if calculatedIndex != nil {
selectedIndex = calculatedIndex!
sendActionsForControlEvents(.ValueChanged)
}
}
return false
}
}
I can explain begin tracking method, the other one is researchable i think so
/*** beginTrackingWithTouch.. method to customize tracking. ***/
// Parameters : touch : this returns the touch that occurred at a certain point in the view. withEvent, returns the UIEvent
// associated with the touch.
// Return Type: Boolean.
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let location = touch.locationInView(self) // This line returns the location of touch in the view. This location is in CGPoint.
var calculatedIndex : Int?
for (index, item) in labels.enumerate() { /// enumeration of an array gives you sequence type of integer and corresponding element type of the array.
if item.frame.contains(location) { /// so labels.enumerate returns the key value pairs like so : [(0, labelatIndex0), (1, labelatIndex1).. and so on.]
calculatedIndex = index /// here index is the key, and item is the value (label.)
// So for every label/index pair, you check whether the touch happened on the label by getting the frame of the label and checking if location is a part of the frame
/// You equate the index to calculatedIndex
}
// Now you if your calculated index is a valid number, you class, which extends UIControl, will call its method stating the change of value(sendActionsForControlEvents).
// This in turn, will send a message to all the targets that have been registered using addTarget:action:forControlEvents: method.
if calculatedIndex != nil {
selectedIndex = calculatedIndex!
sendActionsForControlEvents(.ValueChanged)
}
}
return false
}
Related
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)
}
}
I have a subclass of a UIButton that takes the title label, and puts it under the button's image, as opposed to the right of the image:
final class ImageButton: UIButton {
#IBInspectable var cornerRadius: CGFloat = 8
#IBInspectable var borderColor: UIColor? = .black
private enum Constants {
static let imageSize: CGFloat = 40
static let titleHeight: CGFloat = 12
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.titleRect(forContentRect: contentRect)
}
else {
_ = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - Constants.titleHeight,
width: contentRect.width,
height: Constants.titleHeight
)
}
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.imageRect(forContentRect: contentRect)
} else {
return CGRect(
x: contentRect.width / 2 - Constants.imageSize / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
width: Constants.imageSize,
height: Constants.imageSize
)
}
}
override var intrinsicContentSize: CGSize {
if #available(iOS 15, *) {
return super.intrinsicContentSize
}
else {
_ = super.intrinsicContentSize
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(
width: max(size.width, Constants.imageSize),
height: Constants.imageSize + Constants.titleHeight + spacing
)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
if #available(iOS 15, *) {
var myConfiguration = UIButton.Configuration.plain()
myConfiguration.imagePlacement = .top
self.configuration = myConfiguration
} else {
titleLabel?.textAlignment = .center
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.borderWidth = 1
layer.borderColor = borderColor?.cgColor
}
}
Trying to change the button's title does not have any effect:
myCustomButton.setTitle("Disable Box Select", for: .normal)
I tried adding:
myCustomButton.layer.setNeedsLayout()
myCustomButton.layer.setNeedsDisplay()
But, nothing seems to change the title of myCustomButton
This is not really an answer, but in response to the OP's comment...
Storyboard:
Complete controller code (using your posted ImageButton class, unedited):
class CustBtnVC: UIViewController {
#IBOutlet var myCustomButton: ImageButton!
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Test"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Test", for: .normal)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Disable Box Select"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Disable Box Select", for: .normal)
}
}
}
Note: since your custom button code is setting the button style for iOS 15+ the button .configuration should be used instead of .setTitle(...) when running on iOS 15+
On launch:
after tap on the view:
I've a ULabel on my view controller. So, If user draws line over the UILabel then I've to print text from the UILabel by user drawn range. This is the purpose.
My question is,
How to get character frames from the CGPoint ranges (start & end) on UILabel? I simply want to print the string as I coded.
Diagram:
Drawing:
String Range:
I don't have any clue to solve this problem. So, Please help to find the solution. Thanks in advance...
Code:
import UIKit
class ViewController: UIViewController {
let font: UIFont = UIFont.systemFont(ofSize: 16, weight: .medium)
lazy var label: UILabel = {
let label = UILabel()
label.text = "The orange is the fruit of various citrus"
label.font = font
label.textAlignment = .left
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.leftAnchor.constraint(equalTo: view.leftAnchor),
label.rightAnchor.constraint(equalTo: view.rightAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
label.textColor = .white
label.backgroundColor = .black
let button = UIButton(frame: .init(x: (view.frame.width/2)-30, y: 80, width: 60, height: 40))
button.backgroundColor = .gray
button.setTitle("Print", for: .normal)
button.addTarget(self, action: #selector(actionButton), for: .touchUpInside)
view.addSubview(button)
}
#objc func actionButton(){
//Any demo points.
let p1 = CGPoint.zero
let p2 = CGPoint(x: 70, y: 0)
print("String: ")
}
}
Try this in Swift Playground
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabelExtended()
label.frame = view.bounds
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
label.numberOfLines = 0
label.text = "Lorem ipsum, or lipsum as it is sometimes known, is dummy text used in laying out print, graphic or web designs. The passage is attributed to an unknown typesetter in the 15th century who is thought to have scrambled parts of Cicero's De Finibus Bonorum et Malorum for use in a type specimen book."
label.textColor = .black
view.addSubview(label)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
class UILabelExtended: UILabel {
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
var textStorage = NSTextStorage()
lazy var gesture = TouchGestureRecognizer(target: self, action: #selector(onSwipe(gesture:)))
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
isUserInteractionEnabled = true
addGestureRecognizer(gesture)
}
override var text: String? {
get {
return super.attributedText?.string
}
set {
let attrs: [NSAttributedString.Key : Any] = [.font: font!]
attributedText = NSAttributedString(string: newValue ?? "", attributes: attrs)
updateTextContainer()
}
}
var startIndex = -1
var endIndex = -1
var startPoint = CGPoint.zero
var endPoint = CGPoint.zero
#objc func onSwipe(gesture: UIGestureRecognizer) {
switch gesture.state {
case .began:
updateBoundingBox()
let start = getTextIndexFrom(point: gesture.location(in: self))
startIndex = start.0
startPoint = start.1
case .changed:
let end = getTextIndexFrom(point: gesture.location(in: self))
endIndex = end.0
endPoint = end.1
if let text = attributedText?.string, startIndex > -1 {
if startIndex > endIndex { swap(&startIndex, &endIndex) }
let s = text.index(text.startIndex, offsetBy: startIndex)
let e = text.index(text.startIndex, offsetBy: endIndex)
print(startPoint, endPoint, text[s...e])
}
default:
break
}
setNeedsDisplay()
}
var textBoundingBox: CGRect = .zero
private func updateBoundingBox() {
textBoundingBox = layoutManager.usedRect(for: textContainer)
textBoundingBox.origin = CGPoint(x: bounds.midX - textBoundingBox.size.width / 2, y: bounds.midY - textBoundingBox.size.height / 2)
}
private func getTextIndexFrom(point: CGPoint) -> (Int, CGPoint) {
let touchPoint = CGPoint(x: point.x - textBoundingBox.origin.x, y: point.y - textBoundingBox.origin.y)
let index = layoutManager.characterIndex(for: touchPoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return (index, touchPoint)
}
private func updateTextContainer() {
textStorage = .init(attributedString: attributedText!)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
}
override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(2)
context.move(to: startPoint)
context.addLine(to: endPoint)
context.addRect(textBoundingBox)
context.strokePath()
}
}
}
class TouchGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
delegate = self
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
state = .changed
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return !(otherGestureRecognizer is TouchGestureRecognizer)
}
}
I'm subclassing UISlider and overriding maximumValue to be 5, minimumValue to be 0, isContinuous to be false, and both track tint colors. Is there something I NEED to override to fix this issue, such as draw? I tried setting the slider directly to a value above 1, but it still doesn't go beyond it. Any help would be greatly appreciated.
There's no exceptions, debugging it shows the value that is sent to setValue is changing accordingly, the only issue I'm seeing is that the slider isn't updating visually.
class Slider: UISlider {
private var viewData: ViewData
// MARK: - Initialization
init(viewData: ViewData) {
self.viewData = viewData
super.init(frame: .zero)
render(for: viewData)
translatesAutoresizingMaskIntoConstraints = false
}
override public init(frame: CGRect) {
self.viewData = ViewData(orientation: .vertical, type: .discrete)
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func render(for viewData: ViewData) {
self.addTarget(self, action: #selector(sliderValueDidChange), for: .valueChanged)
}
#objc private func sliderValueDidChange(_ sender: UISlider) {
self.setValue(sender.value, animated: true)
Haptic.triggerImpact(.light)
}
// MARK: - Overrides
override var maximumValue: Float {
get {
switch viewData.type {
case .continuous: return 100
case .discrete: return 5
}
}
set { maximumValue = newValue }
}
override var minimumValue: Float {
get { return 0 }
set { minimumValue = newValue }
}
override var isContinuous: Bool {
get {
switch viewData.type {
case .continuous: return true
case .discrete: return false
}
}
set { isContinuous = newValue }
}
override var minimumTrackTintColor: UIColor? {
get { return .white }
set { minimumTrackTintColor = newValue }
}
override var maximumTrackTintColor: UIColor? {
get { return .gray }
set { minimumTrackTintColor = newValue }
}
override open func trackRect(forBounds bounds: CGRect) -> CGRect {
var newBounds = super.trackRect(forBounds: bounds)
newBounds.size = CGSize(width: 222, height: 10)
return newBounds
}
override open func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
var newThumb = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)
newThumb.size = CGSize(width: 30, height: 30)
return newThumb
}
override open func setValue(_ value: Float, animated: Bool) {
guard viewData.type == .discrete else {
super.setValue(value, animated: animated)
return
}
let roundedValue = Int(value)
super.setValue(Float(roundedValue), animated: animated)
setNeedsDisplay()
Haptic.triggerImpact(.light)
}
All of your overridden properties are causing infinite recursion in the set blocks.
Instead of overriding them just to set a specific value, update your render method to set the desired initial values.
private func render(for viewData: ViewData) {
self.addTarget(self, action: #selector(sliderValueDidChange), for: .valueChanged)
minimumValue = 0
switch viewData.type {
case .continuous: maximumValue = 100
case .discrete: maximumValue = 5
}
isContinuous = viewData.type == .continuous
minimumTrackTintColor = .white
maximumTrackTintColor = .gray
}
Besides this update, remove those overridden properties and the slider will work just fine.
I am new on swift and on my project I've been trying to use custom label such as clickable '#' and '#' sub strings on the related label. I found a library that called as FFLabel on github. I've made some tests for FFLabel. Everything is good, but I want to use different colors for '#' and '#' sub strings. I' ve tried to make some changes on source code. However, it did not work correctly. Also, '#' sub strings is not working(it is not clickable). I guess that there is a regex issue in source code.
Here is the source code of FFLabel:
import UIKit
#objc
public protocol FFLabelDelegate: NSObjectProtocol {
optional func labelDidSelectedLinkText(label: FFLabel, text: String)
}
public class FFLabel: UILabel {
public var linkTextColor = UIColor(red: 0, green: 63.0/255.0, blue: 121.0/255.0, alpha: 1.0)
public weak var labelDelegate: FFLabelDelegate?
// MARK: - override properties
override public var text: String? {
didSet {
updateTextStorage()
}
}
override public var attributedText: NSAttributedString? {
didSet {
updateTextStorage()
}
}
override public var font: UIFont! {
didSet {
updateTextStorage()
}
}
override public var textColor: UIColor! {
didSet {
updateTextStorage()
}
}
// MARK: - upadte text storage and redraw text
private func updateTextStorage() {
if attributedText == nil {
return
}
let attrStringM = addLineBreak(attributedText!)
regexLinkRanges(attrStringM)
addLinkAttribute(attrStringM)
textStorage.setAttributedString(attrStringM)
setNeedsDisplay()
}
/// add link attribute
private func addLinkAttribute(attrStringM: NSMutableAttributedString) {
var range = NSRange(location: 0, length: 0)
var attributes = attrStringM.attributesAtIndex(0, effectiveRange: &range)
attributes[NSFontAttributeName] = font!
attributes[NSForegroundColorAttributeName] = textColor
attrStringM.addAttributes(attributes, range: range)
attributes[NSForegroundColorAttributeName] = linkTextColor
for r in linkRanges {
attrStringM.setAttributes(attributes, range: r)
}
}
/// use regex check all link ranges
private let patterns = ["[a-zA-Z]*://[a-zA-Z0-9/\\.]*", "#.*?#", "#[\\u4e00-\\u9fa5a-zA-Z0-9_-]*"]
private func regexLinkRanges(attrString: NSAttributedString) {
linkRanges.removeAll()
let regexRange = NSRange(location: 0, length: count(attrString.string))
for pattern in patterns {
let regex = NSRegularExpression(pattern: pattern, options: NSRegularExpressionOptions.DotMatchesLineSeparators, error: nil)
let results = regex?.matchesInString(attrString.string, options: NSMatchingOptions(rawValue: 0), range: regexRange)
if let results = results {
for r in results {
linkRanges.append(r.rangeAtIndex(0))
}
}
}
}
/// add line break mode
private func addLineBreak(attrString: NSAttributedString) -> NSMutableAttributedString {
let attrStringM = NSMutableAttributedString(attributedString: attrString)
var range = NSRange(location: 0, length: 0)
var attributes = attrStringM.attributesAtIndex(0, effectiveRange: &range)
var paragraphStyle = attributes[NSParagraphStyleAttributeName] as? NSMutableParagraphStyle
if paragraphStyle != nil {
paragraphStyle!.lineBreakMode = NSLineBreakMode.ByWordWrapping
} else {
// iOS 8.0 can not get the paragraphStyle directly
paragraphStyle = NSMutableParagraphStyle()
paragraphStyle!.lineBreakMode = NSLineBreakMode.ByWordWrapping
attributes[NSParagraphStyleAttributeName] = paragraphStyle
attrStringM.setAttributes(attributes, range: range)
}
return attrStringM
}
public override func drawTextInRect(rect: CGRect) {
let range = glyphsRange()
let offset = glyphsOffset(range)
layoutManager.drawBackgroundForGlyphRange(range, atPoint: offset)
layoutManager.drawGlyphsForGlyphRange(range, atPoint: CGPointZero)
}
private func glyphsRange() -> NSRange {
return NSRange(location: 0, length: textStorage.length)
}
private func glyphsOffset(range: NSRange) -> CGPoint {
let rect = layoutManager.boundingRectForGlyphRange(range, inTextContainer: textContainer)
let height = (bounds.height - rect.height) * 0.5
return CGPoint(x: 0, y: height)
}
// MARK: - touch events
public override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let location = (touches.first as! UITouch).locationInView(self)
selectedRange = linkRangeAtLocation(location)
modifySelectedAttribute(true)
}
public override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let location = (touches.first as! UITouch).locationInView(self)
if let range = linkRangeAtLocation(location) {
if !(range.location == selectedRange?.location && range.length == selectedRange?.length) {
modifySelectedAttribute(false)
selectedRange = range
modifySelectedAttribute(true)
}
} else {
modifySelectedAttribute(false)
}
}
public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if selectedRange != nil {
let text = (textStorage.string as NSString).substringWithRange(selectedRange!)
labelDelegate?.labelDidSelectedLinkText!(self, text: text)
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue()) {
self.modifySelectedAttribute(false)
}
}
}
public override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
modifySelectedAttribute(false)
}
private func modifySelectedAttribute(isSet: Bool) {
if selectedRange == nil {
return
}
var attributes = textStorage.attributesAtIndex(0, effectiveRange: nil)
attributes[NSForegroundColorAttributeName] = linkTextColor
let range = selectedRange!
textStorage.addAttributes(attributes, range: range)
setNeedsDisplay()
}
private func linkRangeAtLocation(location: CGPoint) -> NSRange? {
if textStorage.length == 0 {
return nil
}
let offset = glyphsOffset(glyphsRange())
let point = CGPoint(x: offset.x + location.x, y: offset.y + location.y)
let index = layoutManager.glyphIndexForPoint(point, inTextContainer: textContainer)
for r in linkRanges {
if index >= r.location && index <= r.location + r.length {
return r
}
}
return nil
}
// MARK: - init functions
override public init(frame: CGRect) {
super.init(frame: frame)
prepareLabel()
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepareLabel()
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
private func prepareLabel() {
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
userInteractionEnabled = true
}
// MARK: lazy properties
private lazy var linkRanges = [NSRange]()
private var selectedRange: NSRange?
private lazy var textStorage = NSTextStorage()
private lazy var layoutManager = NSLayoutManager()
private lazy var textContainer = NSTextContainer()
}
Thank you for your answers
King regards
Note: I solved hashtag clickable issue(regular expression issue) by changing the hashtag expression as #[\\u4e00-\\u9fa5a-zA-Z0-9_-]*. But the rest of the issues still going on.
You could also try ActiveLabel.swift which is an UILabel drop-in replacement supporting Hashtags (#), Mentions (#) and URLs (http://) written in Swift.
Maybe that's exactly what you're looking for. Giving usernames, hashtags and links different colors is as simple as this:
label.textColor = .blackColor()
label.hashtagColor = .blueColor()
label.mentionColor = .greenColor()
label.URLColor = .redColor()
Disclaimer: I'm the author of the library.