How to bring a subview to front when selected in Swift? - swift

So I've got two views, both added to the superview programmatically:
//First added view to my superView (baseView)
let imageViewOne = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
baseView.addSubview(imageViewOne)
//Second added view to my superView (baseView)
let imageViewTwo = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
baseView.addSubview(imageViewTwo)
I want to bring one of them to the front when it is selected because when you add a new view, it overlaps the previous one. Something that works with "selected" or "touched".

Use this:
baseView.bringSubview(toFront: imageViewOne)

You can do this by UITapGestureRecognizer by setting Restoration ID from the interface builder
func tapOneAct(sender: UITapGestureRecognizer) {
if(sender.view!.restorationIdentifier == "view1")
// view1 is `Restoration ID` of imageViewOne
{
view.bringSubview(toFront: imageViewOne)
}
if(sender.view!.restorationIdentifier == "view2")
// view2 is `Restoration ID` of 2nd view
{
view.bringSubview(toFront: imageViewTwo)
}
}

If I understand correctly then you would like to check the status of selected view and bring it to the front of the view hierarchy.
In this case you have to do following things:
1) track selection state, let it will be var isSelected: Bool in code snippet
2) hold a callback for an action when view is selected (you can use for example UIButton to observe tap event)
In following code snippet you can check the selection state when the view is touched.
BaseView implements logic for checking the state of SelectionView.
import UIKit
final class SelectionView<T: UIView>: UIView {
var onTap: ((_ isSelected: Bool) -> ())?
// MARK: - Private
private let view: T
private let button = UIButton(type: .custom)
private var isSelected: Bool = false
// MARK: - Init
init(view: T) {
self.view = view
super.init(frame: .zero)
setUp()
}
// MARK: - Private
private func setUp() {
addSubview(view)
addSubview(button)
button.addTarget(self, action: #selector(onButtonTap), for: .touchUpInside)
}
#objc private func onButtonTap() {
isSelected = !isSelected
onTap?(isSelected)
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
view.frame = bounds
button.frame = bounds
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return view.sizeThatFits(size)
}
// MARK: - Unused
override init(frame: CGRect) {
fatalError("init(frame:) has not been implemented")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class BaseView: UIView {
init() {
super.init(frame: .zero)
let views = [SelectionView(view: UIView()), SelectionView(view: UIView())]
for view in views {
view.onTap = { [weak self, weak view] isSelected in
guard let selectedView = view
else { return }
if isSelected {
self?.bringSubview(toFront: selectedView)
}
}
}
}
// MARK: - Unused
override init(frame: CGRect) {
fatalError("init(frame:) has not been implemented")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Related

Custom view for NSMenuItem appearing blank

I've been trying to make a custom view for a NSMenuItem with an image on it. The code below overrides the original view, but without the image. The item appears blank and smaller.
This code is only an example!
class CustomItemView: NSView {
private let imageView: NSImageView = NSImageView()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
// self.imageView.frame = frameRect
self.imageView.image = NSImage.init(named: "example")!
self.addSubview(self.imageView)
self.imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
self.imageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.imageView.widthAnchor.constraint(equalToConstant: 100),
self.imageView.heightAnchor.constraint(equalToConstant: 100)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I then simply add set the view like this
let newItem: NSMenuItem = NSMenuItem()
newItem.view = CustomItemView()
newItem.target = self
self.addItem(newItem)

Swift - get random CGPoints from UIView

I am trying to get random CGPoints along the 'outer contour' of UIView so that I will be able to draw UIBezierPath line over obtained CGPoints. For example, get CGPoints from below red dots which I marked myself by hand. Any idea?
###Edit###
As per Sweeper's advice I am trying to add CAShapeLayer into my custom view but it returns nil when I am printing its path. Please could you point out what I am missing?
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let view = BubbleView(frame: CGRect(x: self.view.frame.midX / 2, y: self.view.frame.midY/2, width: 150, height: 100))
view.backgroundColor = .gray
view.layer.cornerRadius = 30
self.view.addSubview(view)
}
}
class BubbleView: UIView {
var pathLayer: CAShapeLayer!
override func layoutSubviews() {
pathLayer = CAShapeLayer()
pathLayer.bounds = frame
self.layer.addSublayer(pathLayer)
print(pathLayer.path) // returns nil
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

How to create this view with constraints programtically

I feel this is pretty simple to accomplish but I can't seem to figure it out. I'm fairly new to not using the storyboard and trying to learn how to set my constraints programatically for my views. I created the view that I want easily in storyboard but can't seem to get it programatically.
I have my view controller has my parent view, and then I call a container view. I imagine in the container view is where I setup my constraints but I can't get the height of my view to stay the same every-time I change to a different device
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var clariView = ClariContainerView()
view.addSubview(clariView)
}
}
This my view controller and then my ClariContainerView looks like this:
class ClariContainerView: UIView {
lazy var clariQuestionView: UIView = {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0))
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
public func setupView() {
addSubview(clariQuestionView)
setupLayout()
}
public func setupLayout() {
NSLayoutConstraint.activate([
clariQuestionView.heightAnchor.constraint(equalToConstant: 169)
])
}
}
What I'm trying to recreate is this:
I need the height of the blue view to always be 169.
Here is how you would do that:
First, you don't need to define a frame for your containerView since the translatesAutoresizingMaskIntoConstraints = falsestatement is specifying that you'll be using auto-layout and therefore the frame will be ignored:
lazy var clariQuestionView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
And here is how you would define your constraints. You need to set height, but also need to pin the view to the bottom, the leading, and the trailing edges of self.view:
public func setupLayout() {
NSLayoutConstraint.activate([
clariQuestionView.heightAnchor.constraint(equalToConstant: 169),
clariQuestionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
clariQuestionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
clariQuestionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
For such a basic layout you don't really need to add heightAnchor. Here is a simple way to achieve desired behavior + bonus — a code snippet to adjust height according to the device's safeAreaInsets.
class ClariContainerView: UIView {
lazy var clariQuestionView: UIView = {
let desiredContainerHeigh = 169
// If you want, you can use commented code to adjust height according to the device's safe area.
// This might be needed if you want to keep the same height over safe area on all devices.
// let safeAreaAdjustment = UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets.bottom ?? 0
let containerView = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height - 169, width: UIScreen.main.bounds.width, height: 169))
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = true
return containerView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
public func setupView() {
addSubview(clariQuestionView)
}
}

Stacking views programmatically in UIStackView

I have spent a long time trying to stack the views that I create programmatically. I looked at examples from Add views in UIStackView programmatically but that didn't work. Listed below is the code, I am calling the setUpListings from the view controller. There are two entries but only one entry is shown.
import UIKit
import SnapKit
class ListingsView : UIView {
var containerView: UIView!
var listingsContainerView: UIStackView!
init() {
super.init(frame: CGRect.zero)
setUpContainerView()
setUpListingsContainer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setUpContainerView() {
containerView = UIView()
self.addSubview(containerView)
containerView.snp.makeConstraints { (make) in
make.height.equalTo(self)
make.width.equalTo(self)
containerView.backgroundColor = UIColor.white
}
}
func setUpListingsContainer() {
listingsContainerView = UIStackView()
listingsContainerView.distribution = .equalSpacing
listingsContainerView.alignment = .fill
listingsContainerView.axis = .vertical
listingsContainerView.spacing = 10
listingsContainerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(listingsContainerView)
listingsContainerView.snp.makeConstraints { (make) in
make.top.equalTo(containerView)
make.left.equalTo(containerView)
make.bottom.equalTo(containerView)
make.right.equalTo(containerView)
}
}
func setUpListings(listings: [Listing]) {
for listing in listings {
let listingEntry = ListingEntry(listingId: listing.id)
listingsContainerView.addArrangedSubview(listingEntry)
}
}
class ListingEntry : UIView {
var listingId: String?
var containerView: UIView!
init(listingId: String) {
super.init(frame: CGRect.zero)
self.listingId = listingId
self.setUpContainerView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setUpContainerView() {
containerView = UIView()
containerView.backgroundColor = UIColor.gray
self.addSubview(containerView)
containerView.snp.makeConstraints { (make) in
make.width.equalTo(150)
make.height.equalTo(150)
}
}
}
}
The view currently looks like
But the blocks should be stacked.
Couple things...
First, I'd suggest learning how constraints and auto-layout work before using something like SnapKit. It can make some things easier --- but until one has a good understanding of the fundamentals, it's not clear what's doing what.
Second, during development, it helps to give views and subviews contrasting background colors. Makes it much easier to see what's happening to the frames at run-time.
So, if you're going to stick with SnapKit...
Try to keep code "clean." That is, don't put anything inside a snp.makeConstraints block that isn't directly related (such as setting background colors).
In your ListingEntry class, you're adding a subview (containerView) and giving that view a width and height of 150, but you are not constraining it to its superview... which results in a view height of Zero.
Take a look at the modifications I made to your code. I added comments that should make the changes clear:
class MiscViewController: UIViewController {
var listingsView: ListingsView = {
let v = ListingsView()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(listingsView)
listingsView.backgroundColor = .red
// constrain listingsView to all 4 sides with 40-pt "padding"
listingsView.snp.makeConstraints { (make) in
make.top.bottom.leading.trailing.equalToSuperview().inset(40.0)
}
let listings: [Listing] = [
Listing(id: "A"),
Listing(id: "B"),
Listing(id: "C"),
]
listingsView.setUpListings(listings: listings)
}
}
struct Listing {
var id: String = ""
}
class ListingsView : UIView {
var containerView: UIView!
var listingsContainerView: UIStackView!
init() {
super.init(frame: CGRect.zero)
// probably want to set clipsToBounds so any content doesn't extend outside the frame
clipsToBounds = true
setUpContainerView()
setUpListingsContainer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setUpContainerView() {
containerView = UIView()
self.addSubview(containerView)
containerView.backgroundColor = UIColor.green
// constrain containerView to all 4 sides
containerView.snp.makeConstraints { (make) in
make.top.bottom.leading.trailing.equalToSuperview()
}
}
func setUpListingsContainer() {
listingsContainerView = UIStackView()
listingsContainerView.distribution = .equalSpacing
listingsContainerView.alignment = .fill
listingsContainerView.axis = .vertical
listingsContainerView.spacing = 10
containerView.addSubview(listingsContainerView)
// constrain listingsContainerView (a stack view) to all 4 sides
listingsContainerView.snp.makeConstraints { (make) in
make.top.leading.bottom.trailing.equalToSuperview()
}
}
func setUpListings(listings: [Listing]) {
for listing in listings {
let listingEntry = ListingEntry(listingId: listing.id)
listingEntry.backgroundColor = .cyan
listingsContainerView.addArrangedSubview(listingEntry)
}
}
class ListingEntry : UIView {
var listingId: String?
var containerView: UIView!
init(listingId: String) {
super.init(frame: CGRect.zero)
self.listingId = listingId
self.setUpContainerView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setUpContainerView() {
containerView = UIView()
containerView.backgroundColor = .gray
self.addSubview(containerView)
containerView.snp.makeConstraints { (make) in
// you want the "listing container" to be 150 x 150 pts
make.width.equalTo(150)
make.height.equalTo(150)
// and it needs top and bottom constraints to give self a height value
make.top.bottom.equalToSuperview()
// and it needs an x-position constraint
make.leading.equalToSuperview()
}
}
}
}
I've set the "main" ListingsView background color to red ... you don't see it because its containerView subview is green and fills the view.
Each ListingEntry view has a cyan background color, and its containerView has a gray background color.
The result:
and Debug View Hierarchy:
Last notes...
You set your StackView .distribution = .equalSpacing but you also set .spacing = 10, which doesn't make sense.
If you have more ListingEntry views than will fit vertically, you'll run into problems. I'd expect you'd put that into a scroll view.

Custom view's IBOutlet is nil

So I have a main viewController that has a stack view to which I want to add multiple instances of custom views I have created.
So my thought process was to create a custom view that inherits from UIView. I want the view to always be 40x40 so I created a new init to take care of this? (not sure if this is correct):
class QuickAddView: UIView {
#IBOutlet weak var iconLabel: UILabel!
var task: Task = Task()
public init(task: Task) {
self.task = task
super.init(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
configureView()
}
private func configureView() {
self.layer.cornerRadius = self.frame.size.width / 2
self.backgroundColor = task.color
configureViewElements()
}
private func configureViewElements() {
configureIconLabel()
}
private func configureIconLabel() {
// CRASH: - iconLabel is nil here
self.iconLabel.text = task.icon
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
I then have a QuickAddView nib that sets its custom class to QuickAddView.swift
Lastly, I create the custom views in my viewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
configureViewElements()
}
private func configureViewElements() {
configureQuickAddStackView()
}
private func configureQuickAddStackView() {
let quickAddView = QuickAddView(task: Task(name: "Go to the store", icon: "🍐", color: .purple))
quickAddStackView.addArrangedSubview(quickAddView)
}
The problem I'm having is that my iconLabel is nil when I try to set up my QuickAddView. I also don't know if I'm doing this process of creating a custom view correct.
Since you are connecting the IBOutlet from xibFile you have to use
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
configureView()
}
Also in your class you have to instantiate it with
if let quickAddView = Bundle.mainBundle().loadNibNamed("QuickAddView", owner: self, options: nil).first as? QuickAddView {
quickAddView.task = task
}
The problem is that you're creating a custom view in code, but using an IBOutlet for the iconLabel. An IBOutlet is for something you've built in Interface Builder, not in code. If you want to create the QuickAddView in code, you need to also create the iconLabel in code, in the QuickAddView class. You need to make these modifications:
weak var iconLabel: UILabel?
private func configureIconLabel() {
iconLabel = UILabel(frame: CGRect())
if let iconLabel = iconLabel {
iconLabel.text = task.icon
addSubview(iconLabel)
}
}
Note that I passed in a zeroed out CGRect, but you'll want to use whatever you want the UILabels origin and size to be, or use autolayout to configure where the iconLabel is displayed.