How to change NSScroller color? - objective-c

I'm trying to change the color of the scroller itself, it seems I need to overwrite something around drawKnob but I couldn't find any method which seemed appropriate.
One way I thought about was to inject a new style and get scroller to use it but again, I couldn't find anything about NSColor in NSScroller header.
Any idea?

No need to redraw from scratch. It's as simple as this:
import AppKit
class CyanideScroller: NSScroller {
private var _bkgrColour: NSColor = NSColor.black
private var _knobColour: NSColor = NSColor.white
private var isHorizontal = false
var bkgrColour: NSColor {
get { return _bkgrColour}
set { _bkgrColour = newValue}
}
var knobColour: NSColor {
get { return _knobColour}
set { _knobColour = newValue}
}
override var frame: CGRect {
didSet {
let size = frame.size
isHorizontal = size.width > size.height
}
}
override func draw(_ dirtyRect: NSRect) {
_bkgrColour.setFill()
dirtyRect.fill()
self.drawKnob()
}
override func drawKnob() {
_knobColour.setFill()
let dx, dy: CGFloat
if isHorizontal {
dx = 0; dy = 3
} else {
dx = 3; dy = 0
}
let frame = rect(for: .knob).insetBy(dx: dx, dy: dy)
NSBezierPath.init(roundedRect: frame, xRadius: 3, yRadius: 3).fill()
}
}

I think you will have to write a subclass, and override drawKnob, maybe drawKnobSlotInRect: too depending on what you want to change.

The CyanideScroller class is nice, I've just updated it a little bit. Above all I have made sure that the knob is drawn only if necessary. This is my class:
import AppKit
/**
A custom object that controls scrolling of a document view
within a scroll view or other type of container view.
*/
class CustomScroller: NSScroller {
/**
Depository of the horizontal Boolean value.
*/
private var _isHorizontal: Bool = false
/**
A Boolean value that indicates whether the scroller is horizontal.
*/
public var isHorizontal: Bool {
return _isHorizontal
}
/**
The background color.
*/
public var backgroundColor: NSColor = NSColor.black
/**
The knob color.
*/
public var knobColor: NSColor = NSColor.white
/**
A Boolean value that indicates whether the scroller draws its background.
*/
public var drawsBackground: Bool = true
override var frame: CGRect {
didSet {
let size = frame.size
_isHorizontal = size.width > size.height
}
}
override func draw(_ dirtyRect: NSRect) {
if drawsBackground {
backgroundColor.setFill()
dirtyRect.fill()
}
if (usableParts == .allScrollerParts) {
drawKnob()
}
}
/**
Draws the knob.
*/
override func drawKnob() {
knobColor.setFill()
let dx, dy: CGFloat
if _isHorizontal {
dx = 0
dy = 3
} else {
dx = 3
dy = 0
}
let frame = rect(for: .knob).insetBy(dx: dx, dy: dy)
let roundedPath = NSBezierPath(roundedRect: frame, xRadius: 3, yRadius: 3)
roundedPath.fill()
}
}

Related

Swift - UIStackView - hide if height of all items is below threshold

I have UIStackView in vertical mode filled with UIButtons. I have dynamic screen resize and if all buttons in stack view have height under some threshold, I want to hide them. How to achive this automatically?
I have tried to extend UIButton and add:
override func layoutSubviews() {
super.layoutSubviews()
self.isHidden = (self.frame.height < 20)
}
which works, but once the button is hidden it will never re-appear and layoutSubviews is never called back (even if the height should be again larger).
It's not clear what all you are doing, or why you say it would be problematic to set the buttons' .alpha property, but here are two approaches, both using a UIStackView subclass and handling the show/hide in layoutSubviews().
1: calculate what the button heights will be and set .isHidden property:
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 1
// setting .isHidden
let numViews = arrangedSubviews.count
let numSpaces = numViews - 1
let h = (bounds.height - (spacing * CGFloat(numSpaces))) / CGFloat(numViews)
let bHide = h < 20
arrangedSubviews.forEach { v in
v.isHidden = bHide
}
}
}
set .isHidden property based on what the button heights are (much simpler):
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 2
// setting .alpha
arrangedSubviews.forEach { v in
v.alpha = v.frame.height < 20 ? 0.0 : 1.0
}
}
}
And here's a sample controller to see it in use. Tapping anywhere will toggle the height of the stack view between 300 and 100 (buttons will have less-than 20-pts height at 100):
class ConditionalStackViewController: UIViewController {
let stackView: MyStackView = {
let v = MyStackView()
// so we can see the stack view frame
v.backgroundColor = .systemYellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var stackHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...6 {
let b = UIButton()
b.setTitle("Button \(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .red
stackView.addArrangedSubview(b)
}
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
stackHeight = stackView.heightAnchor.constraint(equalToConstant: 300.0)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stackHeight,
])
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
#objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
stackHeight.constant = stackHeight.constant == 300 ? 100 : 300
}
}

UIButton I need to create a button with curved edges and borders but I'm not sure how to do it

I have been searching all over google trying to find an answer to this but can't. I need to create a buttin that has rounded or curved edges as well as adding a border to it. Nothing I yhave tried has worked. please help
I would suggest showing us what you have already tried; that said, here is what I use:
#IBDesignable class MyButton: UIButton
{
#IBInspectable var borderColor:UIColor? {
set {
layer.borderColor = newValue!.cgColor
}
get {
if let color = layer.borderColor {
return UIColor(cgColor:color)
}
else {
return nil
}
}
}
#IBInspectable var borderWidth:CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
#IBInspectable var rounded: Bool = false {
didSet {
updateCornerRadius()
}
}
func updateCornerRadius() {
layer.cornerRadius = rounded ? frame.size.height / 2 : 0
}
}

How do I update a SwiftUI View in UIKit when value changes?

I am trying to integrate a SwiftUI view that animates on changes to a #State variable (originally progress was #State private progress: CGFloat = 0.5 in the SwiftUI View), into an existing UIKit application. I have read through lots of search results on integrating SwiftUI into UIKit (#State, #Binding, #Environment, etc.), but can't seem to figure this out.
I create a very simple example of what I am attempting to do, as I believe once I see this answer I can adopt it to my existing application.
The Storyboard is simply View controller with a UISlider. The code below displays the SwiftUI view, but I want it to update as I move the UISlider.
import SwiftUI
class ViewController: UIViewController {
var progress: CGFloat = 0.5
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let frame = CGRect(x: 20, y: 200, width: 400, height: 400)
let childView = UIHostingController(rootView: Animate_Trim(progress: progress))
addChild(childView)
childView.view.frame = frame
view.addSubview(childView.view)
childView.didMove(toParent: self)
}
#IBAction func sliderAction(_ sender: UISlider) {
progress = CGFloat(sender.value)
print("Progress: \(progress)")
}
}
struct Animate_Trim: View {
var progress: CGFloat
var body: some View {
VStack(spacing: 20) {
Circle()
.trim(from: 0, to: progress) // Animate trim
.stroke(Color.blue,
style: StrokeStyle(lineWidth: 40,
lineCap: CGLineCap.round))
.frame(height: 300)
.rotationEffect(.degrees(-90)) // Start from top
.padding(40)
.animation(.default)
Spacer()
}.font(.title)
}
}```
If you do not want to use NotificationCenter. You could use just #Published and assign or sink.
I wrote a working example in a Playground to show the concept:
//This code runs on Xcode playground
import Combine
import SwiftUI
class ObservableSlider: ObservableObject {
#Published public var value: Double = 0.0
}
class YourViewController {
var observableSlider:ObservableSlider = ObservableSlider()
private var cancellables: Set<AnyCancellable> = []
let hostController = YourHostingController() // I put it here for the sake of the example, but you do need a reference to the Hosting Controller.
init(){ // In a real VC this code would probably be on viewDidLoad
let swiftUIView = hostController.rootView
//This is where values of SwiftUI view and UIKit get glued together
self.observableSlider.$value.assign(to: \.observableSlider.value, on: swiftUIView).store(in:&self.cancellables)
}
func updateSlider() {
observableSlider.value = 8.5
}
}
// In real app it would be something like:
//class YourHostingController<YourSwiftUIView> UIHostingController
class YourHostingController {
var rootView = YourSwiftUIView()
//In a real Hosting controller you would do something like:
// required init?(coder aDecoder: NSCoder){
// super.init(coder: aDecoder, rootView: YourSwiftUIView())
// }
}
struct YourSwiftUIView: View{
var body: some View {
EmptyView() // Your real SwiftUI body...
}
#ObservedObject var observableSlider: ObservableSlider = ObservableSlider()
func showValue(){
print(observableSlider.value)
}
init(){
print(observableSlider.value)
}
}
let yourVC = YourViewController() // Inits view and prints 0.0
yourVC.updateSlider() // Updates from UIKit to 8.5
yourVC.hostController.rootView.showValue() // Value in SwiftUI View is updated (prints 8.5)
The accepted answer actually doesn't answer the original question "update a SwiftUI View in UIKit..."?
IMHO, when you want to interact with UIKit you can use a notification to update the progress view:
extension Notification.Name {
static var progress: Notification.Name { return .init("progress") }
}
class ViewController: UIViewController {
var progress: CGFloat = 0.5 {
didSet {
let userinfo: [String: CGFloat] = ["progress": self.progress]
NotificationCenter.default.post(Notification(name: .progress,
object: nil,
userInfo: userinfo))
}
}
var slider: UISlider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
slider.addTarget(self, action: #selector(sliderAction(_:)), for: .valueChanged)
slider.frame = CGRect(x: 0, y: 500, width: 200, height: 50)
// Do any additional setup after loading the view.
let frame = CGRect(x: 20, y: 200, width: 400, height: 400)
let childView = UIHostingController(rootView: Animate_Trim())
addChild(childView)
childView.view.frame = frame
view.addSubview(childView.view)
view.addSubview(slider)
childView.didMove(toParent: self)
}
#IBAction func sliderAction(_ sender: UISlider) {
progress = CGFloat(sender.value)
print("Progress: \(progress)")
}
}
struct Animate_Trim: View {
#State var progress: CGFloat = 0
var notificationChanged = NotificationCenter.default.publisher(for: .progress)
var body: some View {
VStack(spacing: 20) {
Circle()
.trim(from: 0, to: progress) // Animate trim
.stroke(Color.blue,
style: StrokeStyle(lineWidth: 40,
lineCap: CGLineCap.round))
.frame(height: 300)
.rotationEffect(.degrees(-90)) // Start from top
.padding(40)
.animation(.default)
.onReceive(notificationChanged) { note in
self.progress = note.userInfo!["progress"]! as! CGFloat
}
Spacer()
}.font(.title)
}
}
Combine is your friend ...
create the model
update the model from UISlider, or any other part of your application
use the model to update your SwiftUI.View
I did simple example, solely with SwiftUI, but using the same scenario
import SwiftUI
class Model: ObservableObject {
#Published var progress: Double = 0.2
}
struct SliderView: View {
#EnvironmentObject var slidermodel: Model
var body: some View {
// this is not part of state of the View !!!
// the bindig is created directly to your global EnnvironmentObject
// be sure that it is available by
// creating the SwiftUI view that provides the window contents
// in your SceneDelegate.scene(....)
// let model = Model()
// let contentView = ContentView().environmentObject(model)
let binding = Binding<Double>(get: { () -> Double in
self.slidermodel.progress
}) { (value) in
self.slidermodel.progress = value
}
return Slider(value: binding, in: 0.0 ... 1.0)
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
Circle()
.trim(from: 0, to: CGFloat(model.progress)) // Animate trim
.stroke(Color.blue,
style: StrokeStyle(lineWidth: 40,
lineCap: CGLineCap.round))
.frame(height: 300)
.rotationEffect(.degrees(-90)) // Start from top
.padding(40)
.animation(.default)
SliderView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
and all is nicely working together

tvos: UITextView focus appearance like movies App

I am building a tvos app and i want the UITextView to behave similarly like in tvos Movies app. I am specially interested in the focused appearence. Please have a look ate these two pictures.
Currently i am just adding background color to the textview when it is focused but how i can achieve this focused appearance in the attached images. here is my small code
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
if context.previouslyFocusedView == lessonDescriptionTxt {
coordinator.addCoordinatedAnimations({ () -> Void in
self.lessonDescriptionTxt.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
if context.nextFocusedView == lessonDescriptionTxt {
coordinator.addCoordinatedAnimations({ () -> Void in
self.lessonDescriptionTxt.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
}
}
Also if someone can suggest how i can achieve this MORE feature in the textView when there is more text. I also read this question Make UILabel focusable and tappable (tvOS) but that does not do the job for me.
The "MORE" text appears only when the description can't fit in the space that's available. You should be able to measure the amount of space needed by text by using UIKit's -[NSAttributedString boundingRectWithSize:options:context:] method.
Implementing custom FocusableTextView class worked for me:
class FocusableTextView: UITextView {
let suffixWithMore = " ... MORE"
weak var tapDelegate: FocusableTextViewDelegate?
var currentText = ""
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
isScrollEnabled = true // must be set to true for 'stringThatFitsOnScreen' function to work
isUserInteractionEnabled = true
isSelectable = true
textAlignment = .justified
self.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
tap.allowedPressTypes = [NSNumber(value: UIPress.PressType.select.rawValue)]
self.addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// use setText(_:) function instead of assigning directly to text variable
func setText(_ txt: String) {
currentText = txt
let stringThatFits = stringThatFitsOnScreen(originalString: txt) ?? txt
if txt <= stringThatFits {
self.text = txt
} else {
let newString = makeStringWithMORESuffix(from: stringThatFits)
self.text = newString
}
}
func makeStringWithMORESuffix(from txt: String) -> String {
let newNSRange = NSMakeRange(0, txt.count - suffixWithMore.count)
let stringRange = Range(newNSRange,in: txt)!
let subString = String(txt[stringRange])
return subString + suffixWithMore
}
func stringThatFitsOnScreen(originalString: String) -> String? {
// the visible rect area the text will fit into
let userWidth = self.bounds.size.width - self.textContainerInset.right - self.textContainerInset.left
let userHeight = self.bounds.size.height - self.textContainerInset.top - self.textContainerInset.bottom
let rect = CGRect(x: 0, y: 0, width: userWidth, height: userHeight)
// we need a new UITextView object to calculate the glyphRange. This is in addition to
// the UITextView that actually shows the text (probably a IBOutlet)
let tempTextView = UITextView(frame: self.bounds)
tempTextView.font = self.font
tempTextView.text = originalString
// get the layout manager and use it to layout the text
let layoutManager = tempTextView.layoutManager
layoutManager.ensureLayout(for: tempTextView.textContainer)
// get the range of text that fits in visible rect
let rangeThatFits = layoutManager.glyphRange(forBoundingRect: rect, in: tempTextView.textContainer)
// convert from NSRange to Range
guard let stringRange = Range(rangeThatFits, in: originalString) else {
return nil
}
// return the text that fits
let subString = originalString[stringRange]
return String(subString)
}
#objc func tapped(_ gesture: UITapGestureRecognizer) {
print("user selected TextView!")
tapDelegate?.userSelectedText(currentText)
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if (context.nextFocusedView == self) {
backgroundColor = .white
textColor = .black
}
else {
backgroundColor = .clear
textColor = .white
}
}
}
protocol FocusableTextViewDelegate: AnyObject {
func userSelectedText(_ txt: String)
}
When user taps the text View, you can present Alert with full text likewise:
extension YourViewController: FocusableTextViewDelegate{
func userSelectedText(_ txt: String) {
let alert = UIAlertController(title: "", message: txt, preferredStyle: .alert)
let action = UIAlertAction( title: nil, style: .cancel) {_ in }
alert.addAction(action)
self.present(alert, animated: true)
}
}
Usage:
create FocusableTextView programmatically:
assign constraints programmatically to textView (or use frame)
set text to FocusableTextView with setText(_:) method
assing your UIViewController to be the tapDelegate
override func viewDidLoad() {
super.viewDidLoad()
let textView = FocusableTextView(frame: CGRect(x: 0, y: 0,
width: 500,
height: 300),
textContainer: nil)
textView.setText("Your long text..")
textView.tapDelegate = self
}

In swift is is possible to separate set, get, willSet, and willGet from the declaration of the property?

I will start by showing you some properties from a project I'm working on...
/** Properties **/
var coordinates: Coordinates
var text: NSString = "" {
didSet {
self.needsDisplay = true
}
}
var boxViewGroup: GridBoxViewGroup
var isLocked: Bool = false {
willSet {
if isSelected || !newValue {
boxViewGroup.selectedBox = nil
}
}
}
var isSelected: Bool {
get {
if boxViewGroup.selectedBox {
return boxViewGroup.selectedBox === self
}
else {return false}
}
}
var invertedFrame: NSRect {
get {
return NSRect(x: frame.origin.x,
y: superview.bounds.height - frame.origin.y,
width: bounds.size.width,
height: bounds.size.height)
}
set {
self.frame = NSRect(x: newValue.origin.x,
y: superview.bounds.height - newValue.origin.y - newValue.height,
width: newValue.width,
height: newValue.height)
}
}
That looks a little messy right. So my question is is it possible to put get, set, willGet, and willSet methods in a separate place so that my property declarations can look like this...
var coordinates: Coordinates
var boxViewGroup: GridBoxViewGroup
var isSelected: Bool
var invertedFrame: NSRect
See like this I can actually tell what properties there are.
It's possible by spiting in into 2 classes. You can override property declaration in subclass and add Property Observers
class DataA {
var coordinates: Coordinates
var boxViewGroup: GridBoxViewGroup
var isSelected: Bool
var invertedFrame: NSRect
}
class A : DataA {
override var coordinates: Coordinates {
didSet {
//add code
}
willSet(newValue) {
//add code
}
}
}
Read more about Property Overriding in apple documentation