How to properly make a container view that contains some viewControllers(programmatically)? - ios

I want to create a container View programmatically that has inside a bunch of view controllers. I have a segmented controller that when the user tapped a button it should display a certain viewController without doing a segue.
I have everything inside a ViewController,
I wanted to know how to properly make the segmented controller display a certain view controller when tapped.
func setUpSegmentedControl() {
let seg = UISegmentedControl(items: ["1", "2", "3"])
seg.selectedSegmentIndex = 0
seg.translatesAutoresizingMaskIntoConstraints = false
seg.layer.cornerRadius = 8
seg.backgroundColor = UIColor.white
seg.addTarget(self, action: #selector(changeColor(sender:)), for: .valueChanged)
view.addSubview(seg)
NSLayoutConstraint.activate([
seg.centerXAnchor.constraint(equalTo: view.centerXAnchor), seg.centerYAnchor.constraint(equalTo: view.centerYAnchor), seg.leadingAnchor.constraint(equalTo: view.leadingAnchor), seg.trailingAnchor.constraint(equalTo: view.trailingAnchor), seg.heightAnchor.constraint(equalToConstant: 50)
])
}
#objc func changeColor(sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
addChild(FirstViewController())
print("1")
case 1:
addChild(SecondViewController())
print("2")
default:
addChild(ThirdViewController())
print("3")
}
}

To add a view controller to a container view programmatically, you need to call addSubview and didMove in addition to addChild.
let firstViewController = FirstViewController()
addChild(firstViewController)
firstViewController.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(firstViewController.view) // replace `containerView` with the name of the view that's supposed to contain the VC's view
// add constraints or set frame manually
let trailingConstraint = firstViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let leadingConstraint = firstViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let topConstraint = firstViewController.view.topAnchor.constraint(equalTo: view.topAnchor)
let bottomConstraint = firstViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([trailingConstraint, leadingConstraint, topConstraint, bottomConstraint])
firstViewController.didMove(to parent: self)
If you want to remove a child view controller programatically:
let child = children.first // or other way to identify your VC
child?.willMove(toParentViewController: nil)
child?.view.removeFromSuperview()
child?.view.removeFromParentViewController()
Example project

Related

messageInputBar not dismissing when I dismiss my MessagesViewController (MessageKit, begging for help!)

I am using MessageKit. I've created a MessagesViewController. I add messageInputBar as a subview from viewDidLoad along with a navigational bar that includes a back button. Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController. If I don't tap on the messageInputBar when i first enter the MessagesViewController and press the back button, the messageInputBar properly is dismissed. Below is my code
override func viewDidLoad() {
super.viewDidLoad()
setUpNavBar()
navigationItem.largeTitleDisplayMode = .never
maintainPositionOnKeyboardFrameChanged = true
scrollsToLastItemOnKeyboardBeginsEditing = true
messageInputBar.inputTextView.tintColor = .systemBlue
messageInputBar.sendButton.setTitleColor(.systemTeal, for: .normal)
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
loadChat()
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
}
func setUpNavBar() {
let navBar = UINavigationBar()
self.view.addSubview(navBar)
navBar.items?.append(UINavigationItem(title: (selectedUser?.userFirstName)!))
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonTapped))
navBar.topItem?.leftBarButtonItem = backButton
navBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
navBar.heightAnchor.constraint(equalToConstant: 44),
navBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
#IBAction func backButtonTapped(_ sender: Any) {
let transition = CATransition()
self.view.window!.layer.add(transition.segueLeftToRight(), forKey: kCATransition)
self.dismiss(animated: false)
}
As you have created MessagesViewController, you don't need to explicitly add messageInputBar to the bottom of the view.
Let's look at the source of MessageKit
private func setupInputBar(for kind: MessageInputBarKind) {
inputContainerView.subviews.forEach { $0.removeFromSuperview() }
func pinViewToInputContainer(_ view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
inputContainerView.addSubviews(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
])
}
switch kind {
case .messageInputBar:
pinViewToInputContainer(messageInputBar)
case .custom(let view):
pinViewToInputContainer(view)
}
}
The following code section should be removed from your source as the messageInputBar has already been set up in the library.
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
Now, your scenario
Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController.
Whenever, there is an object that you interact with (eg. messageInputBar), and it is not deallocated (stays in view) after you dismissed the view controller, there is a memory leak.
If you repeatedly enter and dismiss the view controller, you will observe a rise in the memory usage of the app. So, finding out which object is creating this retain cycle, should solve this issue.
I installed MessageKit using Cocoapods later to find out they dropped support for Cocoapods. So I completely migrated my entire project over to Swift Package Manager to get the latest MessageKit which includes the setup for the inputbar in their code. No idea why they would release a version that didn't have this initially? Anyways, solved my problem!

Hiding UIButton(which added programatically) once view is hidden

Goal:
Hide a UIButton(w/ image) once a view is hidden.
I have a layout whereby the map view can be hidden when user taps(UITapGestureRecognizer) on the screen. When this happen, I would like to hide the "follow user button" triangle. Currently I am not able to do it.
What I've tried: (from multiple google/SO posts)
1)
followUserButton.removeFromSuperview()
followUserButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 150).isActive = true
followUserButton.setImage(image:nil for: .normal)
the last one I tried is basically just making the image black in color (to blend into the background). This does look like a success, but (see gif image), for some reason, the first click, will still show the button (very light black/grey - in the bottom middle of image). Click again, the map view comes on, then click again and it finally disappears
followUserButton.tintColor = .black
followUserButton.isHidden = true
this is how I'm adding my button programatically
var followUserImage: UIImage!
var followUserButton: UIButton!
override func viewDidLoad() {
setupFollowUserButton()
}
func setupFollowUserButton() {
addFollowUserButton()
self.view.addSubview(followUserButton)
constraintFollowUserButton()
}
func hideFollowUserButton() {
if vcTrainMapView.isHidden {
if followUserButton != nil {
// followUserButton.removeFromSuperview()
// followUserButton.tintColor = .black
followUserButton.isHidden = true
}
} else if followUserButton != nil {
followUserButton.tintColor = .lightGray
}
}
func addFollowUserButton() {
followUserButton = UIButton(type: UIButton.ButtonType.custom)
followUserImage = UIImage(named: "follow_user_high")
followUserButton.setImage(followUserImage, for:.selected)
followUserImage = (UIImage(named: "follow_user"))
followUserButton.setImage(followUserImage, for: .normal)
followUserButton.tintColor = .lightGray
followUserButton.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
}
#objc private func buttonAction(_ sender: UIButton) {
self.followUserStatus = !self.followUserStatus
sender.isSelected.toggle()
}
func constraintFollowUserButton() {
followUserButton.translatesAutoresizingMaskIntoConstraints = false
followUserButton.bottomAnchor.constraint(equalTo: vcTrainMapView.bottomAnchor, constant: -10).isActive = true
followUserButton.leadingAnchor.constraint(equalTo: vcTrainMapView.leadingAnchor, constant: 10).isActive = true
followUserButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
followUserButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
this is what I've achieved w/ #3 above. (the button is very light in the gif)
If you're going to be hiding and showing a button at the same time as a detail view (your map view), and you're displaying the button so that it looks like it's on that view, you could just add the button directly to that view rather than the view controller main view.
You can, of course, still control the action of the button from the view controller, but if it's added to the map view then the button will be hidden when the map view is hidden.

Is this an iOS 14 UISplitViewController bug?

In my app, I want a three-column UISplitViewController. I create it like this:
let svc = UISplitViewController(style: .tripleColumn)
svc.preferredDisplayMode = .twoOverSecondary
svc.setViewController(TestViewController(), for: .primary)
svc.setViewController(TestViewController(), for: .supplementary)
svc.setViewController(TestViewController(), for: .secondary)
svc.primaryBackgroundStyle = .sidebar
This ViewController that I'm presenting is a really simple viewController. It doesn't do anything except, present a centered red square.
class TestViewController: UIViewController {
private var redView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(redView)
redView.widthAnchor.constraint(equalToConstant: 30).isActive = true
redView.heightAnchor.constraint(equalToConstant: 30).isActive = true
redView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
redView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
}
}
Now, when I run this code, I get the following result.
As you can see, 2 of the 3 squares are off-center. Anyone know, what I'm doing wrong here? Or is this a known bug?
You should use "Safe Area" instead of "self.view" when setting to center.
The detail view here has a push-away effect. You just need to click on it and the primary view controller will hide, like below.

ContainerView - how close invisible view

I have this storyboard:
and this code:
var actualVisibleView : String? = nil
func showSubViewInContainerView(view: String){
let controller = storyboard!.instantiateViewController(withIdentifier: view)
addChildViewController(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
systemContainerView.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: systemContainerView.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: systemContainerView.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: systemContainerView.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: systemContainerView.bottomAnchor)
])
controller.didMove(toParentViewController: self)
if self.actualVisibleView != nil && self.actualVisibleView != view {
controller.dismiss(animated: false) {
print("UBIJAM: \(view)")
}
}
self.actualVisibleView = view
print("OTWIERAM: \(view)")
}
From the left menu, I open various views in this containerview using the code:
showSubViewInContainerView(view: "view1")
showSubViewInContainerView(view: "view2")
showSubViewInContainerView(view: "view3")
showSubViewInContainerView(view: "view4")
This code works light. The only problem is that when I open a new view in the container view I would like to close the previously visible view.
Only one active view will be visible in containerview.
At the moment there are views overlapping one another.
Does anyone know how to fix it?
Before adding a new view in the container remove other views
systemContainerView.subviews.forEach { $0.removeFromSuperview() }
systemContainerView.addSubview(controller.view)

UIStackView & Gestures

I'm trying to get/keep a handle on elements in my UIStackView after I have moved it with a pan gesture.
For example, the elements I am trying to grab a handle on are things like the button tag or the text label text.
The code explained…
I am creating a UIStackView, via the function func createButtonStack(label: String, btnTag: Int) -> UIStackView
It contains a button and a text label.
When the button stack is created, I attach a pan gesture to it so I can move the button around the screen. The following 3 points work.
I get the button stack created
I can press the button and call the fun to print my message.
I can move the button stack.
The issue I have is…
Once I move the button stack the first time and the if statement gesture.type == .ended line is triggered, I lose control of the button stack.
That is, the button presses no longer work nor can I move it around any longer.
Can anyone please help? Thanks
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
let ButtonStack = createButtonStack(label: “Button One”, btnTag: 1)
view.addSubview(ButtonStack)
ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
ButtonStack.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
let panGuesture = UIPanGestureRecognizer(target: self, action: #selector(pan(guesture:)))
ButtonStack.isUserInteractionEnabled = true
ButtonStack.addGestureRecognizer(panGuesture)
}
func createButtonStack(label: String, btnTag: Int) -> UIStackView {
let button = UIButton()
button.setImage( imageLiteral(resourceName: "star-in-circle"), for: .normal)
button.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
button.contentMode = .scaleAspectFit
button.tag = btnTag
switch btnTag {
case 1:
button.addTarget(self, action: #selector(printMessage), for: .touchUpInside)
case 2:
break
default:
break
}
//Text Label
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.green
textLabel.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
textLabel.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
textLabel.font = textLabel.font.withSize(15)
textLabel.text = label
textLabel.textAlignment = .center
//Stack View
let buttonStack = UIStackView()
buttonStack.axis = UILayoutConstraintAxis.vertical
buttonStack.distribution = UIStackViewDistribution.equalSpacing
buttonStack.alignment = UIStackViewAlignment.center
buttonStack.spacing = 1.0
buttonStack.addArrangedSubview(button)
buttonStack.addArrangedSubview(textLabel)
buttonStack.translatesAutoresizingMaskIntoConstraints = false
return buttonStack
}
#objc func printMessage() {
print(“Button One was pressed”)
}
#objc func pan(guesture: UIPanGestureRecognizer) {
let translation = guesture.translation(in: self.view)
if let guestureView = guesture.view {
guestureView.center = CGPoint(x: guestureView.center.x + translation.x, y: guestureView.center.y + translation.y)
if guesture.state == .ended {
print("Guesture Center - Ended = \(guestureView.center)")
}
}
guesture.setTranslation(CGPoint.zero, in: self.view)
}
If you're using autolayout on the buttonStack you can't manipulate the guestureView.center centerX directly. You have to work with the constraints to achieve the drag effect.
So instead of ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true you should do something along the lines of:
let centerXConstraint = ButtonStack.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
centerXConstraint.isActive = true
ButtonStack.centerXConstraint = centerXConstraint
To do it like this you should declare a weak property of type NSLayoutConstraint on the ButtonStack class. You can do the same thing for the centerY constraint.
After that in the func pan(guesture: UIPanGestureRecognizer) method you can manipulate the centerXConstraint and centerYConstraint properties directly on the ButtonStack view.
Also, I see you are not setting the translatesAutoresizingMaskIntoConstraints property to false on the ButtonStack. You should do that whenever you are using autolayout programatically.
Thanks to PGDev and marosoaie for their input. Both provided insight for me to figure this one out.
My code worked with just the one button, but my project had three buttons inside a UIStackView.
Once I moved one button, it effectively broke the UIStackView and I lost control over the moved button.
The fix here was to take the three buttons out of the UIStackView and I can now move and control all three buttons without issues.
As for keeping a handle on the button / text field UIStackView, this was achieved by adding a .tag to the UIStackView.
Once I moved the element, the .ended action of the pan could access the .tag and therefore allow me to identify which button stack was moved.
Thanks again for all of the input.

Resources