I want to stop the user interacting with a tableview while I get some data.
So I can show and remove a view such that:
var dimmingView: UIView?
Is added and removed with:
func showLoading() {
let dimmingView = UIView(frame: view.frame)
dimmingView.backgroundColor = .darkGray
view.addSubview(dimmingView)
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
dimmingView.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: dimmingView.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: dimmingView.centerYAnchor)
])
activityIndicator.startAnimating()
self.dimmingView = dimmingView
}
And also
func removeLoading() {
dimmingView?.removeFromSuperview()
}
However, when added it moves along with the table (scrolls). If I stop the table scrolling when the new data is loaded it presents behind the UINavigationBar (the first cell at least). If I try to add to the navigationcontroller.view navigation.controller is nil (so I can't).
What is the best approach?
Related
Problem
I have a custom UIView that has an image and selection (border) subview. I want to be able to add this custom UIView as a subview of a larger blank view. Here's the catch, the larger blank view needs to clip all of the subviews to its bounds (clipToBounds). However, the user can select one of the custom UIViews within the large blank view, where the subview is then highlighted by a border.
The problem is that because the large blank view clips to bounds, the outline for the selected subview is cut off.
I want the image in the subview to clip to the bounds of the large blank view, but still be able to see the full selection outline of the subview (which is cut off due to the large blank view's corner radius.
I am using UIKit and Swift
👎 What I Currently Have:
👍 What I Want:
The image part of the subview clips to the bounds (corner radius) of the large blank view, but the outline selection view in the subview should not.
Thanks in advance for all your help!
I think what you are looking for is not technically possible as defined by the docs
From the docs:
clipsToBounds
Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.
So the subviews do not have control of whether they get clipped or not, it's the container view that decides.
So I believe Matic's answer is right in that the structure he proposes gives you the most flexibility.
With that being said, here are a couple of work arounds I can think of:
First, set up to recreated your scenario
Custom UIView
// Simple custom UIView with image view and selection UIView
fileprivate class CustomBorderView: UIView
{
private var isSelected = false
{
willSet
{
toggleBorder(newValue)
}
}
var imageView = UIImageView()
var selectionView = UIView()
init()
{
super.init(frame: CGRect.zero)
configureImageView()
configureSelectionView()
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
}
private func configureImageView()
{
imageView.image = UIImage(named: "image-test")
imageView.contentMode = .scaleAspectFill
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
private func configureSelectionView()
{
selectionView.backgroundColor = .clear
selectionView.layer.borderWidth = 3
selectionView.layer.borderColor = UIColor.clear.cgColor
addSubview(selectionView)
selectionView.translatesAutoresizingMaskIntoConstraints = false
selectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
selectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
selectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
selectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
configureTapGestureRecognizer()
}
private func configureTapGestureRecognizer()
{
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(didTapSelectionView))
selectionView.addGestureRecognizer(tapGesture)
}
#objc
private func didTapSelectionView()
{
isSelected = !isSelected
}
private func toggleBorder(_ on: Bool)
{
if on
{
selectionView.layer.borderColor = UIColor(red: 28.0/255.0,
green: 244.0/255.0,
blue: 162.0/255.0,
alpha: 1.0).cgColor
return
}
selectionView.layer.borderColor = UIColor.clear.cgColor
}
}
Then in the view controller
class ClippingTestViewController: UIViewController
{
private let mainContainerView = UIView()
private let customView = CustomBorderView()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
title = "Clipping view"
configureMainContainerView()
configureCustomBorderView()
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
}
private func configureMainContainerView()
{
mainContainerView.backgroundColor = .white
view.addSubview(mainContainerView)
mainContainerView.translatesAutoresizingMaskIntoConstraints = false
mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
mainContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 20).isActive = true
mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
mainContainerView.heightAnchor.constraint(equalToConstant: 300).isActive = true
view.layoutIfNeeded()
}
private func configureCustomBorderView()
{
mainContainerView.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
customView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor).isActive = true
customView.topAnchor.constraint(equalTo: mainContainerView.safeAreaLayoutGuide.topAnchor).isActive = true
customView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor).isActive = true
customView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor).isActive = true
view.layoutIfNeeded()
}
}
This gives me your current experience
Work Around 1. - Shrink subviews on selection
When the view is not selected, everything looks fine. When the view is selected, you could reduce the width and height of the custom subview with some animation while adding the border.
Work Around 2. - Manually clip desired subviews
You go through each subview in your container view and:
Apply the clipping to any subview you desire
Apply the corner radius to the views you clip
Leaving the container view unclipped and without a corner radius
To do that, I created a custom UIView subclass for the container view
class ClippingSubView: UIView
{
override var clipsToBounds: Bool
{
didSet
{
if clipsToBounds
{
clipsToBounds = false
clipImageViews(in: self)
layer.cornerRadius = 0
}
}
}
// Recursively go through all subviews
private func clipImageViews(in view: UIView)
{
for subview in view.subviews
{
// I am only checking image view, you could check which you want
if subview is UIImageView
{
print(layer.cornerRadius)
subview.layer.cornerRadius = layer.cornerRadius
subview.clipsToBounds = true
}
clipImageViews(in: subview)
}
}
}
Then make sure to adjust the following lines where you create your views:
let mainContainerView = ClippingSubView()
// Do this only after you have added all the subviews for this to work
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
This gives me your desired output
This is a pretty common problem which may have multiple solutions. In the end though I always find it best to simply go one level higher:
ContainerView (Does not clip)
ContentView (Clips)
HighlightingView (Does not clip)
You would put all your current views on ContentView. Then introduce another view which represents your selection and put it on the same level as your ContentView.
In the end this will give you most flexibility. It can still get a bit more complicated when you add things like shadows. But again "more views" is usually the end solution.
You'll likely run into a lot of problems trying to get a subview's border to display outside its superView's clipping bounds.
One approach is to add an "Outline View" as a sibling of the "Clipping View":
When you select a clippingView's subview - and drag it around - set the frame of the outlineView to match the frame of that subview.
You'll want to set .isUserInteractionEnabled = false on the outlineView so it doesn't interfere with touches on the subviews.
I'm trying to implement an input accessory view that works just like Messages app in iOS. I've searched almost every SO questions regarding this topic, but couldn't find the solution that worked for me.
Here is the minimal reproducible code I created, referring to this SO post.
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
becomeFirstResponder() // seems unnecessary
}
override var inputAccessoryView: UIToolbar {
return self.keyboardAccessory
}
override var canBecomeFirstResponder: Bool {
return true
}
var textView: UITextView = {
let view = UITextView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var keyboardAccessory: UIToolbar = {
let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
inputAccessory.addSubview(textView)
NSLayoutConstraint.activate([
textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
textView.widthAnchor.constraint(equalToConstant: 200),
textView.heightAnchor.constraint(equalToConstant: 50)
])
inputAccessory.backgroundColor = .gray
return inputAccessory
}()
}
Every article I've seen suggests overriding inputAccessoryView and canBecomeFirstResponder, and that's it. However, the keyboard does not appear until I tap the textView.
Can anyone let me know what I'm missing?
Edit
As #DonMag pointed out, Messages app in iOS does not show keyboard automatically. Please consider following UI in Facebook instead.
When I press the comment button, it pushes to another view controller while popping up the keyboard. The transition effect doesn't have to be exactly the same, but I want the keyboard become fully loaded within presented view controller, as if I called becomeFirstResponder() in viewDidLoad.
If you want the text view to become active, and the keyboard to show, as soon as the view appears, use:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
textView.becomeFirstResponder()
}
If you want the text view to be visible at the bottom, and become active / show the keyboard when the textview is tapped, take a look at this answer:
https://stackoverflow.com/a/61508928/6257435
Edit
If you want to push a view controller onto the navigation stack, and have the keyboard showing with a custom input accessory view, containing a text view, and give it the focus...
Add a hidden text field to the controller. In viewDidLoad tell that text field to use the custom input accessory view and tell it to become first responder.
Then, in viewDidAppear tell the text view in the custom input accessory view to become the first responder:
class TestViewController: UIViewController {
var hiddenTF = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// set the text field to hidden
hiddenTF.isHidden = true
// add it to the view
view.addSubview(hiddenTF)
// tell hidden text field to use custom input accessory view
hiddenTF.inputAccessoryView = keyboardAccessory
// tell it to become first responder
hiddenTF.becomeFirstResponder()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// tell the textView (in the custom input accessory view)
// to become first responder
textView.becomeFirstResponder()
}
var textView: UITextView = {
let view = UITextView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var keyboardAccessory: UIToolbar = {
let inputAccessory = UIToolbar(frame: .init(x: 0, y: 0, width: 0, height: 100))
inputAccessory.addSubview(textView)
NSLayoutConstraint.activate([
textView.centerXAnchor.constraint(equalTo: inputAccessory.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: inputAccessory.centerYAnchor),
textView.widthAnchor.constraint(equalToConstant: 200),
textView.heightAnchor.constraint(equalToConstant: 50)
])
inputAccessory.backgroundColor = .gray
return inputAccessory
}()
}
Actually when you override canBecomeFirstResponder the keyboard is appear just under the view , thats why you only see the accessory view bottom side of the view . You can basically try this with adding notification to your controller like
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name:UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyboardWillShow(notification:NSNotification) {
let userInfo = notification.userInfo!
let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
print(keyboardFrame)
print(self.view.frame)
}
When you run the project , you gonna see the keyboardWillShow notification is hired.(If you delete overiride canBecomeFirstResponder it won't )
And when you print keyboard and view frame , you gonna notice to keyboard's y position is equal to view's frame height . That means keyboards want to show us only its accessoryView .
So , you need to hired textView.becomeFirstResponder() in keyboardWillShow notification
#objc func keyboardWillShow(notification:NSNotification) {
let userInfo = notification.userInfo!
let keyboardFrame:CGRect = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
textView.becomeFirstResponder()
}
Do not forget to deinit notification when controller deinit
deinit {
NotificationCenter.default.removeObserver(self)
}
So I found the following UI pattern online and I have been attempting to implement it in Xcode. However, I have been unsuccessful. I am unsure as to whether to create the optimal approach would be to
create three different UIViewControllers (in which case I am not sure as to how to get them to animate in and out of view/how to get them to overlap one another)
or to use a UITableView with custom overlapping cells. However, I am not sure whether this approach will allow me to animate properly upon pressing each cell. For this second approach, I saw this post, but this solution does not allow for touch interaction in the overlapping areas, something which I need.
I looked for libraries online that would allow for functionality such as this, but I was unsuccessful in finding any. The animation I am trying to achieve can be found here.
I would use a StackView to hold all the views of your ViewControllers.
Then you can set the stack view's spacing property to a negative value to make the views overlap. If you wish show the complete view of one of the view controllers on tap, you add a tap gesture recognizer to that view that changes the stackView's spacing for just that view to 0.
A really simple example:
class PlaygroundViewController: UIViewController {
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
lazy var viewControllersToAdd = [firstViewController, secondViewController, thirdViewController]
let heightOfView: CGFloat = 300
let viewOverlap: CGFloat = 200
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
firstViewController.view.backgroundColor = .red
secondViewController.view.backgroundColor = .blue
thirdViewController.view.backgroundColor = .green
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = -viewOverlap
viewControllersToAdd.forEach { (controller: UIViewController) in
if let childView = controller.view {
stackView.addArrangedSubview(childView)
NSLayoutConstraint.activate([
childView.heightAnchor.constraint(equalToConstant: heightOfView),
childView.widthAnchor.constraint(equalTo: stackView.widthAnchor)
])
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapChildView(sender:)))
childView.addGestureRecognizer(gestureRecognizer)
childView.isUserInteractionEnabled = true
}
addChild(controller)
controller.didMove(toParent: self)
}
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
])
}
#objc func didTapChildView(sender: UITapGestureRecognizer) {
if let targetView = sender.view {
UIView.animate(withDuration: 0.3, animations: {
let currentSpacing = self.stackView.customSpacing(after: targetView)
if currentSpacing == 0 {
// targetView is already expanded, collapse it
self.stackView.setCustomSpacing(-self.viewOverlap, after: targetView)
} else {
// expand view
self.stackView.setCustomSpacing(0, after: targetView)
}
})
}
}
}
Edit1: I am not using a storyboard everything has been added programmatically
Edit2: Updated code snippet
Hello, i've got the following issues with UICollectionView
The CollectionView won't allow you to scroll no matter what you do.
Sometimes the cells even disappear complete leaving you with a white background CollectionView.
The problem occurs when you try to scroll up to view the cells that are not visible.
I've create a CollectionView which has different type of cells.
The CollectionView is nested inside a ViewController below an ImageView.
Constraints are added and they work just fine.
How do i make it scrollable?
GIF representing the problem
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.navigationItem.title = "Featured"
self.view.backgroundColor = UIColor.white
// Add UImage carousel
let carousel: UIImageView = {
let imageView = UIImageView()
let image = UIImage()
imageView.backgroundColor = UIColor.purple
return imageView
}()
self.view.addSubview(carousel)
carousel.translatesAutoresizingMaskIntoConstraints = false
// Add CollectionView
let featuredControllerLayout = UICollectionViewFlowLayout()
// Add CollectionViewController
featuredControllerLayout.scrollDirection = .vertical
let featuredController = FeaturedCollectionViewController(collectionViewLayout: featuredControllerLayout)
guard let featuredView = featuredController.collectionView else { return }
self.view.addSubview(featuredView)
featuredView.translatesAutoresizingMaskIntoConstraints = false
// Setup Constraints
if #available(iOS 11.0, *) {
let guide = self.view.safeAreaLayoutGuide
let guideSize = guide.layoutFrame.size
carousel.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
carousel.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
carousel.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
carousel.frame.size.height = guideSize.width/2
carousel.heightAnchor.constraint(equalToConstant: carousel.frame.size.height).isActive = true
featuredView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
featuredView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
featuredView.topAnchor.constraint(equalTo: carousel.bottomAnchor).isActive = true
featuredView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
}
}
Heyy what you are telling is you are not able to scroll up and see the entire collection view items ?
if that is the case then you have not accounted the height of tabbar controller so you can enable translucent for example I have a git repo I made you can check and let me know if you are stuck anywhere
https://github.com/dwivediashish00/socialApp
Is it possible to display an UIView on top of a container View?
I want to add a view with a few opacity background to still see my container View. But everything i tried made either my containerView disappear completely or on top of my View. I tried via Storyboard and code.
I'm sure I'm missing something.
just add your view to the view property of your container controller's container
simple:
let viewYouWantToAddSubviewTo = parent?.view
detail:
import UIKit
class CustomNavigationViewController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
let layout = UICollectionViewFlowLayout()
let rootVC = HomeCollectionViewController(collectionViewLayout: layout)
viewControllers = [rootVC]
let v = UIView()
v.backgroundColor = UIColor.blue
v.layer.opacity = 0.4
v.translatesAutoresizingMaskIntoConstraints = false
// add your view to this view of the controller's container
let vv = (parent?.view)!
vv.addSubview(v)
// constraints for v
v.leftAnchor.constraint(equalTo: vv.leftAnchor).isActive = true
v.rightAnchor.constraint(equalTo: vv.rightAnchor).isActive = true
v.topAnchor.constraint(equalTo: vv.topAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: vv.bottomAnchor).isActive = true
}
}
result: