Прокрутка страницы до курсора - xcode

There is a page with several elements:
ImageView
TextView - for the title
Button
TextView - for description
Button
All these elements are in ScrollView.
Question: how to make it so that when editing text from TextView, the page turns to where the cursor is located?
Since the size of the TextView depends on the size of the user's text, I need to disable scrolling in the TextView. And then I don't know how to scroll the page to where the cursor is.
I haven't done much yet, I can't figure out how to do it…
My Code:
import UIKit
class DreamPageViewController: UIViewController {
private let dreamTasks = DreamTasksViewController()
private let scrollView = UIScrollView()
private let contentView = UIView()
private var cancelExecutionDream = false
private let headerImage: UIImageView = {
let image = UIImageView(image: UIImage(systemName: "multiply.square.fill"))
image.clipsToBounds = true
image.layer.cornerRadius = 40
image.contentMode = .scaleAspectFill
image.backgroundColor = .blue
image.tintColor = .systemBlue
return image
}()
private let dreamName: UITextView = {
let title = UITextView()
//label.numberOfLines = 0
title.isScrollEnabled = false
title.text = "Title"
title.font = .systemFont(ofSize: 23, weight: .bold)
return title
}()
private let doneButton: UIButton = {
let button = UIButton()
button.layer.cornerRadius = 20
button.layer.borderWidth = 2.5
button.layer.borderColor = UIColor.systemGray3.cgColor
button.setTitle("Выполнить", for: .normal)
button.setTitleColor(.systemGray2, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 20.0, weight: .semibold)
button.addTarget(self, action: #selector(DoneButtonAction), for: .touchUpInside)
return button
}()
private let textDreamPage: UITextView = {
let text = UITextView()
text.font = .systemFont(ofSize: 18, weight: .medium)
text.textColor = .darkGray
text.isScrollEnabled = false
//text.numberOfLines = 0
text.text = "Lots of text... "
return text
}()
private let tasksButton: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.layer.cornerRadius = 20
button.addTarget(self, action: #selector(dreamTasksAction), for: .touchUpInside)
return button
}()
private let taskButtonTitle: UILabel = {
let label = UILabel()
label.text = "Задачи"
label.textColor = .white
label.font = .systemFont(ofSize: 23, weight: .semibold)
return label
}()
private let tasksCountButton: UIButton = {
let button = UIButton()
let taskCount = DreamTasksViewController()
button.setTitle(String(taskCount.tasks.count), for: .normal)
button.tintColor = .white
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
setupScrollView()
componentsConfigure()
}
#objc func DoneButtonAction(sender: UIButton!) {
let animation = ButtonAnimation()
if cancelExecutionDream == false {
doneButton.setTitle("Выполнено!", for: .normal)
doneButton.layer.borderWidth = 0
doneButton.backgroundColor = #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1)
doneButton.setTitleColor(.white, for: .normal)
animation.animationButton(doneButton)
cancelExecutionDream.toggle()
} else if cancelExecutionDream == true {
doneButton.layer.borderWidth = 2.5
doneButton.layer.borderColor = UIColor.systemGray3.cgColor
doneButton.setTitle("Выполнить", for: .normal)
doneButton.backgroundColor = .none
doneButton.setTitleColor(.systemGray2, for: .normal)
animation.animationButton(doneButton)
cancelExecutionDream.toggle()
} else { return }
}
#objc func dreamTasksAction(sender: UIButton) {
dreamTasks.title = "Задачи"
navigationController?.pushViewController(dreamTasks, animated: true)
}
private func setupUI() {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.backButtonTitle = " "
}
private func setupScrollView() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: -50).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -10).isActive = true
}
private func componentsConfigure() {
contentView.addSubview(headerImage)
contentView.addSubview(dreamName)
contentView.addSubview(doneButton)
contentView.addSubview(textDreamPage)
contentView.addSubview(tasksButton)
tasksButton.addSubview(taskButtonTitle)
tasksButton.addSubview(tasksCountButton)
[headerImage, dreamName, doneButton, textDreamPage, tasksButton, taskButtonTitle, tasksCountButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
NSLayoutConstraint.activate([
headerImage.topAnchor.constraint(equalTo: contentView.topAnchor),
headerImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 7),
headerImage.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -7),
headerImage.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor, multiplier: 3.3/5),
dreamName.topAnchor.constraint(equalTo: headerImage.bottomAnchor, constant: 10),
dreamName.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
dreamName.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
dreamName.heightAnchor.constraint(equalToConstant: 40),
doneButton.topAnchor.constraint(equalTo: dreamName.bottomAnchor, constant: 10),
doneButton.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
doneButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
doneButton.heightAnchor.constraint(equalToConstant: 60),
textDreamPage.topAnchor.constraint(equalTo: doneButton.bottomAnchor, constant: 10),
textDreamPage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 10),
textDreamPage.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10),
textDreamPage.bottomAnchor.constraint(equalTo: tasksButton.topAnchor, constant: -10),
tasksButton.topAnchor.constraint(equalTo: textDreamPage.bottomAnchor, constant: 25),
tasksButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20),
tasksButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
tasksButton.heightAnchor.constraint(equalToConstant: 70),
tasksButton.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.95/1),
taskButtonTitle.centerYAnchor.constraint(equalTo: tasksButton.centerYAnchor),
taskButtonTitle.leftAnchor.constraint(equalTo: tasksButton.leftAnchor, constant: 15),
tasksCountButton.heightAnchor.constraint(equalToConstant: 20),
tasksCountButton.centerYAnchor.constraint(equalTo: tasksButton.centerYAnchor),
tasksCountButton.rightAnchor.constraint(equalTo: tasksButton.rightAnchor, constant: -15)
])
}
}

Related

NSTextView not visible

I want to create a Bordered Rectangle with a Text/Label positioned in the top-left with AppKit that looks like:
The text view should have the same border and background color as the rectangle's border, and a padding around the actual text, not a fixed width/height constant.
I rewrote the above view in AppKit with NSTextView, but I can't see the textview, just the border:
class JumperView: NSView {
let label: String
private let borderWidth: CGFloat = 4
private let borderColor = NSColor(red: 250 / 255, green: 255 / 255, blue: 0 / 255, alpha: 1).cgColor
private let cornerRadius: CGFloat = 8
private var labelView: NSTextView!
required init(frame: NSRect, label: String) {
self.label = label
super.init(frame: frame)
wantsLayer = true
layer!.borderWidth = borderWidth
layer!.borderColor = borderColor
layer!.cornerRadius = cornerRadius
setupLabelView()
self.translatesAutoresizingMaskIntoConstraints = false
self.labelView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.labelView.topAnchor.constraint(equalTo: self.topAnchor),
self.labelView.leftAnchor.constraint(equalTo: self.leftAnchor),
])
}
private func setupLabelView() {
labelView = NSTextView()
labelView.string = label
labelView.font = .systemFont(ofSize: 24, weight: .bold)
// labelView.textContainerInset = .init(width: 8, height: 4)
labelView.wantsLayer = true
labelView.layer!.borderWidth = borderWidth
labelView.layer!.borderColor = borderColor
labelView.layer!.cornerRadius = cornerRadius
labelView.layer!.backgroundColor = borderColor
labelView.backgroundColor = NSColor(cgColor: borderColor)!
addSubview(labelView)
}
override init(frame frameRect: NSRect) {
fatalError()
}
required init?(coder: NSCoder) {
fatalError()
}
}
I'm not sure why the text view isn't showing up. Would appreciate the help!

How draw a rectangle hole on UIBlurEffect and move it on x axis (UIKit)

I'm trying to create blur effect on a view and than add a shape which will show image on this blurred layer (custom video editing functionality)
Currently I'm able to do it only dragging mask view from the right edge:
but when I try to do it from the left edge, I get such a effect:
func configureBlurView() {
let viewHeight: CGFloat = 60
let padding: CGFloat = 10
blurView = UIView()
blurView.layer.cornerRadius = 10
blurView.clipsToBounds = true
blurView.isHidden = true
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
addConstraints([
blurView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding),
blurView.bottomAnchor.constraint(equalTo: stackView.topAnchor, constant: -padding),
blurView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding),
blurView.heightAnchor.constraint(equalToConstant: viewHeight)
])
addBlurEffect(for: blurView)
}
private func addBlurEffect(for view: UIView) {
let blurEffect = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurEffect.alpha = 0.5
blurEffect.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurEffect)
addConstraints([
blurEffect.topAnchor.constraint(equalTo: view.topAnchor),
blurEffect.leadingAnchor.constraint(equalTo: view.leadingAnchor),
blurEffect.bottomAnchor.constraint(equalTo: view.bottomAnchor),
blurEffect.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
private func makeClearHole(rect: CGRect) {
let maskLayer = CAShapeLayer()
maskLayer.fillColor = UIColor.black.cgColor
let pathToOverlay = CGMutablePath()
pathToOverlay.addRect(blurView.bounds)
pathToOverlay.addRect(rect)
maskLayer.path = pathToOverlay
maskLayer.fillRule = .evenOdd
maskLayer.cornerRadius = 10
blurView.layer.mask = maskLayer
}
I'm using touchesMoved method to change orange view dimensions:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard trimmerView.isHidden == false else { return }
if let touch = touches.first{
let currentTouchPoint = touch.location(in: self)
let previousTouchPoint = touch.previousLocation(in: self)
let deltaX = currentTouchPoint.x - previousTouchPoint.x
if trimmerView.bounds.width >= 70 {
if touchStartEdge.middle {
if trimmerViewLeadingConstraint.constant < 10 {
trimmerViewLeadingConstraint.constant = 10
} else if trimmerViewTrailingConstraint.constant > -10 {
trimmerViewTrailingConstraint.constant = -10
} else {
trimmerViewLeadingConstraint.constant += deltaX
trimmerViewTrailingConstraint.constant += deltaX
}
}
if touchStartEdge.leftEdge {
if trimmerViewLeadingConstraint.constant >= 10.0 {
trimmerViewLeadingConstraint.constant += deltaX
} else if trimmerViewLeadingConstraint.constant < 10.0 {
trimmerViewLeadingConstraint.constant = 10
}
}
if touchStartEdge.rightEdge {
if trimmerViewTrailingConstraint.constant <= -10 {
trimmerViewTrailingConstraint.constant += deltaX
} else if trimmerViewTrailingConstraint.constant > -10 {
trimmerViewTrailingConstraint.constant = -10.0
}
}
}
updateProgressBarConstraints()
makeClearHole(rect: CGRect(x: 0, y: 0, width: trimmerView.frame.width, height: trimmerView.frame.height))
UIView.animate(withDuration: 0.10, delay: 0, options: .curveEaseIn) { [weak self] in
self?.layoutIfNeeded()
}
}
}
What I'd like to achieve is to remove blur effect only in bounds of orange view.
Any ideas ?? :)
Thanks for help!!
Couple ways to do this - here's one...
Add a mask to the blur effect view. As the user drags the "trimmer" update the mask.
Here's a quick example...
We'll:
create a stack view with 10 images
overlay that with a masked blur effective view
add a "draggable trimmer view"
when we drag the trimmer, we update the mask
Example View Controller
class TrimmerVC: UIViewController {
var blurView: MaskedBlurView!
let trimmerView = DragView()
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
// respect safe area when we setup constraints
let g = view.safeAreaLayoutGuide
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
stackView.heightAnchor.constraint(equalToConstant: 80.0),
])
// let's add 10 imageviews to the stack view
for i in 1...10 {
if let img = UIImage(systemName: "\(i).circle.fill") {
let imgView = UIImageView(image: img)
imgView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
stackView.addArrangedSubview(imgView)
}
}
let blurEffect = UIBlurEffect(style: .dark)
blurView = MaskedBlurView(effect: blurEffect)
blurView.alpha = 0.5
blurView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(blurView)
NSLayoutConstraint.activate([
blurView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: 0.0),
blurView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
blurView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
blurView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 0.0),
])
trimmerView.backgroundColor = .systemOrange
view.addSubview(trimmerView)
trimmerView.didDrag = { [weak self] newX in
guard let self = self else { return }
self.blurView.clearX = newX - self.stackView.frame.origin.x
}
}
// we'll use this to update the framing when the stack view width changes
// such as on device rotation
var curStackW: CGFloat = -1
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if curStackW != stackView.frame.width {
curStackW = stackView.frame.width
var r = stackView.frame
r.origin.y += r.size.height + 20.0
r.size.width = 160
r.size.height = 40
trimmerView.frame = r
blurView.clearWidth = trimmerView.frame.width
blurView.clearX = 0
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// toggle the trimmer view between
// below the stack view and
// overlaid on the stack view
if trimmerView.frame.origin.y > stackView.frame.origin.y {
let r = stackView.frame
trimmerView.frame.origin.y = r.origin.y - 6.0
trimmerView.frame.size.height = r.height + 12.0
} else {
let r = stackView.frame
trimmerView.frame.origin.y = r.origin.y + r.height + 12.0
trimmerView.frame.size.height = 60.0
}
}
}
Example Draggable "Trimmer" View
class DragView: UIView {
var didDrag: ((CGFloat) -> ())?
let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
maskLayer.fillColor = UIColor.red.cgColor
layer.mask = maskLayer
}
override func layoutSubviews() {
super.layoutSubviews()
let pathToOverlay = CGMutablePath()
pathToOverlay.addRect(bounds)
pathToOverlay.addRect(bounds.insetBy(dx: 20.0, dy: 8.0))
maskLayer.path = pathToOverlay
maskLayer.fillRule = .evenOdd
maskLayer.cornerRadius = 10
}
var touchStartX: CGFloat = 0
var frameStartX: CGFloat = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
touchStartX = touch.location(in: self.superview!).x
frameStartX = self.frame.origin.x
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let loc = touch.location(in: self.superview!)
self.frame.origin.x = frameStartX + (loc.x - touchStartX)
didDrag?(self.frame.origin.x)
}
}
Example Masked Blur View
class MaskedBlurView: UIVisualEffectView {
public var clearWidth: CGFloat = 100 {
didSet { updateMask() }
}
public var clearX: CGFloat = 0 {
didSet { updateMask() }
}
private let maskLayer = CAShapeLayer()
override init(effect: UIVisualEffect?) {
super.init(effect: effect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
maskLayer.fillColor = UIColor.red.cgColor
layer.mask = maskLayer
}
func updateMask() {
let leftR = CGRect(x: 0, y: 0, width: clearX, height: bounds.height)
let rightR = CGRect(x: clearX + clearWidth, y: 0, width: bounds.width, height: bounds.height)
let bez = UIBezierPath(rect: leftR)
bez.append(UIBezierPath(rect: rightR))
maskLayer.path = bez.cgPath
}
override func layoutSubviews() {
super.layoutSubviews()
maskLayer.frame = bounds
}
}
When running (in landscape orientation) it will start like this:
I placed the "trimmer" view below the stack view to make it a little more clear what's happening.
As we drag the trimmer view, the blur view's mask will be updated:
Tapping anywhere in an empty part of the screen will toggle the trimmer view between "under the stack view" and "overlaid on the stack view":
This was just put together quickly -- you should have no trouble restructuring the code to wrap everything into a single custom view (or however it would work best for your needs).

Swift 5 & sheetPresentationController passing data

I am learning swift and trying to pass a variable to a sheet but it isn't changing the variable when the sheet pops up, is there a different way to do this?
let bottomSheet = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "bottomSheet")
if let sheet = bottomSheet.sheetPresentationController{
sheet.detents = [.medium()]
sheet.prefersGrabberVisible = true
sheet.preferredCornerRadius = 24
sheet.prefersGrabberVisible = true
}
bottomSheet.isEnabled = true
self.present(bottomSheet, animated: true, completion: nil)
And in the bottomSheet view controller I have the variable
var isEnabled: Bool = false
But even though I put true it always shows as false
Try it like this, what you need to do is specify it as a view controller.
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let bottomSheet = mainStoryboard.instantiateViewController(withIdentifier: "bottomSheet") as? BottomSheetVC{
if let sheet = bottomSheet.sheetPresentationController{
sheet.detents = [.medium()]
sheet.prefersGrabberVisible = true
sheet.preferredCornerRadius = 24
sheet.prefersGrabberVisible = true
}
bottomSheet.isEnabled = true
self.present(bottomSheet, animated: true)
}
sheetPresentationController only work for ios 15 and later, for previously versions you need to set .custom modalPresentationStype
controller.modalPresentationStyle = .pageSheet
if #available(iOS 15.0, *) {
if let sheet = controller.sheetPresentationController {
sheet.detents = [.medium()]
}
} else {
controller.modalPresentationStyle = .custom
controller.transitioningDelegate = self
}
self.present(controller, animated: true, completion: nil)
// MARK: - UIViewControllerTransitioningDelegate
extension CPPdfPreviewVC: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
PresentationController(presentedViewController: presented, presenting: presenting)
}
and Add Presentation controller as given
class PresentationController: UIPresentationController {
let blurEffectView: UIVisualEffectView!
var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
let blurEffect = UIBlurEffect(style: .dark)
blurEffectView = UIVisualEffectView(effect: blurEffect)
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.blurEffectView.isUserInteractionEnabled = true
self.blurEffectView.addGestureRecognizer(tapGestureRecognizer)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * 0.4),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
0.6))
}
override func presentationTransitionWillBegin() {
self.blurEffectView.alpha = 0
self.containerView?.addSubview(blurEffectView)
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.blurEffectView.alpha = 0.7
}, completion: { (UIViewControllerTransitionCoordinatorContext) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
self.blurEffectView.alpha = 0
}, completion: { (UIViewControllerTransitionCoordinatorContext) in
self.blurEffectView.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView!.roundCorners([.topLeft, .topRight], radius: 22)
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
blurEffectView.frame = containerView!.bounds
}
#objc func dismissController(){
self.presentedViewController.dismiss(animated: true, completion: nil)
}
}
extension UIView {
func roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
}

Continuously Redrawing a Path with Updated Data

I am developing an audio visualizer MacOS app, and I want to use Quartz/CoreGraphics to render the time-varying spectrum coordinated with the playing audio. My Renderer code is:
import Cocoa
class Renderer: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.white.setFill()
bounds.fill()
guard let context = NSGraphicsContext.current?.cgContext else {return}
var x : CGFloat = 0.0
var y : CGFloat = 0.0
context.beginPath()
context.move(to: CGPoint(x: x, y: y))
for bin in 0 ..< 300 {
x = CGFloat(bin)
y = CGFloat(Global.spectrum[bin])
context.addLine(to: CGPoint(x: x, y: y))
}
context.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
context.setLineWidth(1.0)
context.strokePath()
self.setNeedsDisplay(dirtyRect)
}
}
This draws the path once - using the initial all-zeroes values of the spectrum[] array - and then continues to draw that same all-zeroes line indefinitely. It does not update using the new values in the spectrum[] array. I used a print() statement to verify that the values themselves are being updated, but the draw function does not redraw the path using the updated spectrum values. What am I doing wrong?
The following demo shows how to update an NSView with random numbers created by a timer in a separate class to hopefully mimic your project. It may be run in Xcode by setting up a Swift project for MacOS, copy/pasting the source code into a new file called 'main.swift', and deleting the AppDelegate supplied by Apple. A draw function similar to what you posted is used.
import Cocoa
var view : NSView!
var data = [Int]()
public extension Array where Element == Int {
static func generateRandom(size: Int) -> [Int] {
guard size > 0 else {
return [Int]()
}
return Array(0..<size).shuffled()
}
}
class DataManager: NSObject {
var timer:Timer!
#objc func fireTimer() {
data = Array.generateRandom(size:500)
view.needsDisplay = true
}
func startTimer(){
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}
func stopTimer() {
timer?.invalidate()
}
}
let dataMgr = DataManager()
class View: NSView {
override func draw(_ rect: NSRect) {
super.draw(rect)
NSColor.white.setFill()
bounds.fill()
guard let gc = NSGraphicsContext.current?.cgContext else {return}
var xOld : CGFloat = 0.0
var yOld : CGFloat = 0.0
var xNew : CGFloat = 0.0
var yNew : CGFloat = 0.0
var counter : Int = 0
gc.beginPath()
gc.move(to: CGPoint(x: xOld, y: yOld))
for i in 0 ..< data.count {
xNew = CGFloat(counter)
yNew = CGFloat(data[i])
gc.addLine(to: CGPoint(x: xNew, y: yNew))
xOld = xNew;
yOld = yNew;
counter = counter + 1
}
gc.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
gc.setLineWidth(1.0)
gc.strokePath()
}
}
class ApplicationDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#objc func myStartAction(_ sender:AnyObject ) {
dataMgr.startTimer()
}
#objc func myStopAction(_ sender:AnyObject ) {
dataMgr.stopTimer()
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
data = Array.generateRandom(size: 500)
let _wndW : CGFloat = 800
let _wndH : CGFloat = 600
window = NSWindow(contentRect: NSMakeRect( 0, 0, _wndW, _wndH ), styleMask:[.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
// **** Start Button **** //
let startBtn = NSButton (frame:NSMakeRect( 30, 20, 95, 30 ))
startBtn.bezelStyle = .rounded
startBtn.title = "Start"
startBtn.action = #selector(self.myStartAction(_:))
window.contentView!.addSubview (startBtn)
// **** Stop Button **** //
let stopBtn = NSButton (frame:NSMakeRect( 230, 20, 95, 30 ))
stopBtn.bezelStyle = .rounded
stopBtn.title = "Stop"
stopBtn.action = #selector(self.myStopAction(_:))
window.contentView!.addSubview (stopBtn)
// **** Custom view **** //
view = View( frame:NSMakeRect(20, 60, _wndW - 40, _wndH - 80))
view.autoresizingMask = [.width, .height]
window.contentView!.addSubview (view)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let applicationDelegate = ApplicationDelegate()
// **** main.swift **** //
let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = applicationDelegate
application.activate(ignoringOtherApps:true)
application.run()

How to make the frame of a button custom shape in Swift 2

I want the button to be reactive to tap only in the custom polygonal shape that I create and not in the CGRect frame.
button.frame only supports CGRect.
Here is an example of a button that only responds to touches within a certain area.
class MyButton: UIButton {
var path: UIBezierPath!
override func awakeFromNib() {
backgroundColor = UIColor.greenColor()
addTarget(self, action: #selector(touchDown), forControlEvents: .TouchDown)
}
override func drawRect(rect: CGRect) {
path = UIBezierPath()
path.moveToPoint(CGPointMake(150, 10))
path.addLineToPoint(CGPointMake(200, 10))
path.addLineToPoint(CGPointMake(150, 100))
path.addLineToPoint(CGPointMake(100, 100))
path.closePath()
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.path = path.CGPath
layer.addSublayer(shapeLayer)
}
func touchDown(button: MyButton, event: UIEvent) {
if let touch = event.touchesForView(button)?.first {
let location = touch.locationInView(button)
if path.containsPoint(location) == false {
button.cancelTrackingWithEvent(nil)
}
}
}
}
If you want to do it in Swift 3/4:
class MyButton: UIButton {
var path: UIBezierPath!
override func awakeFromNib() {
backgroundColor = UIColor.green
addTarget(self, action: #selector(touchDown), for: .touchDown)
}
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path.move(to: CGPoint(x: 150, y: 10))
path.addLine(to: CGPoint(x: 200, y: 10))
path.addLine(to: CGPoint(x: 150, y: 100))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
}
func touchDown(button: MyButton, event: UIEvent) {
if let touch = event.touches(for: button)?.first {
let location = touch.location(in: button)
if path.contains(location) == false {
button.cancelTracking(with: nil)
}
}
}
}

Resources