I am learning to program views in Xcode instead of using the .storyboard.
I do not want the view to rotate whenever it is being rotated.
I have this so far.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .white
let imageView = UIImageView(image: #imageLiteral(resourceName: "facebook_logo.png"))
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 300).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 100).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
I think the link provided by #oaccamsrazer above is the one you would need. To help out, I've implemented a small example project.
There are two view controllers, linked by a segue (and both wrapped in a UINavigationController).
In the AppDelegate you need the following:
var restrictRotation:UIInterfaceOrientationMask = .all
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
{
return self.restrictRotation
}
And viewWillAppear (since it is called when we traverse through the stack, so is better than using viewDidLoad) we
override func viewWillAppear(_ animated: Bool) {
(UIApplication.shared.delegate as! AppDelegate).restrictRotation = .all
}
The second view will NOT rotate. It just features a label.
in viewDidLoad (you could also use viewWillAppear)
override func viewDidLoad() {
super.viewDidLoad()
(UIApplication.shared.delegate as! AppDelegate).restrictRotation = .portrait
}
Using the storyboard or code only makes no difference, the implementation will still be the same.
Related
I'm working on a project in UIKit, without storyboards (only programmatic layout constraints) and, following this, I have a custom view controller like this:
#objc public class testController: UIViewController, QLPreviewControllerDataSource {
public override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
self.view.translatesAutoresizingMaskIntoConstraints = false
previewController.view.widthAnchor.constraint(equalToConstant: 200).isActive = true
present(previewController, animated: true, completion: nil)
}
public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
guard let url = Bundle.main.url(forResource: String("beans"), withExtension: "pdf") else {
fatalError("Could not load \(index).pdf")
}
return url as QLPreviewItem
}
}
Then, in my main View Controller file, I add this testController as a subview like so:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let test = testController()
self.view.addSubview(test.view)
test.view.translatesAutoresizingMaskIntoConstraints = false
}
}
This works fine, but I'd like to be able to change my testController's programmatic layout constraints relative to it's parent view.
I've tried stuff like this in the main view controller (ViewController):
let test = testController()
self.view.addSubview(test.view)
test.view.translatesAutoresizingMaskIntoConstraints = false
test.view.widthAnchor.constraint(equalTo: 200, constant: 0).isActive = true
but this simply doesn't work/the view doesn't reflect these constraints at all and it seems like the only way I can successfully modify the constraints of the testController, is within the viewDidAppear function of the testController class.
However, if I try something like this:
public override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
self.view.translatesAutoresizingMaskIntoConstraints = false
previewController.view.widthAnchor.constraint(equalToConstant: 200).isActive = true //notice how this works since it's a hardcoded 200
previewController.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0).isActive = true //this throws an error
present(previewController, animated: true, completion: nil)
}
I get an error thrown.
So I'd somehow like to access the parent of testViewController I guess, and use it for the constraints of the view. I've tried unsuccessfully using presentingViewController and parent for this, but they either return nil or throw an error.
Any help here would be appreciated.
This is sample to add view and change the constraints, in your example you have to add more constraint to test view.
class ViewController: UIViewController {
let buttonTest: UIButton = {
let button = UIButton()
button.setTitle("go to ", for: .normal)
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.addSubview(buttonTest)
buttonTest.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonTest.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
buttonTest.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
])
}
#objc func buttonPressed() {
let secondView = SecondViewController()
self.view.addSubview(secondView.view)
secondView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
secondView.view.topAnchor.constraint(equalTo: self.view.topAnchor,constant: 100),
secondView.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
secondView.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
secondView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -100)
])
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
}
}
I'm doing all my UI programatically, to avoid a massive view controller I have a class of type UIView where I'm declaring all my UI elements.
I'm declaring my scrollView like this:
class RegisterUIView: UIView {
lazy var scrollView: UIScrollView = {
let scroll: UIScrollView = UIScrollView()
scroll.contentSize = CGSize(width: self.frame.size.width, height: self.frame.size.height)
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
scrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
}
}
After the declaration. I create an instance of RegisterUIView in my ViewController.
And in the viewWillAppear and viewWillDisappear I used the variable hidesBarsOnSwipe, to hide the navigation bar.
When I scroll down the bar hides, but when I scroll up the bar is not unhiding.
I read in other question here that I need to set the top constraint to the superview.
How can is set the constraints to the superview?, when I try to set it the app crashes, and is obviously because there is no superview.
class RegisterViewController: UIViewController {
private let registerView: RegisterUIView = {
let view: RegisterUIView = RegisterUIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.hidesBarsOnSwipe = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.hidesBarsOnSwipe = false
}
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
func setupLayout() {
view.addSubview(registerView)
NSLayoutConstraint.activate([
registerView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
registerView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
registerView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
registerView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor)
])
}
Add the scroll view to the subview before adding constraints. Your app crashes because you're adding constraints to an object, not in the subview.
view.addSubview(scrollView)
add that line of code to the top of your setupLayout() function before your start adding constraints.
I want to present a fullscreen ViewController "A" to cover our loading process across ViewControllers "B" AND "C", or in other words, 1) I present ViewController A from ViewController B, 2) segue from ViewController B to ViewController C while ViewController A is showing, 3) dismiss the ViewController A into ViewController C that ViewController B segued into.
If I push from the presenter ViewController B, the presented ViewController A will disappear as well. So my question is, what's the best way to change the ViewControllers B and C in the background, while another one (ViewController A) is presented on top of them?
Thanks.
You can do this in two ways:
1.Using a navigation controller
if let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? JunctionDetailsVC {
if let navigator = navigationController {
navigator.pushViewController(viewController, animated: false)
}
}
2.Present modally from you initial VC
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? LoginVC
{
present(vc, animated: false, completion: nil)
}
Remember to not animate because you said that users shouldn't notice the transition.
Without knowing anything else about your app, I think you'd be better off redesigning the flow and User Experience, but here is one approach to do what you want.
We start with VC-B as the root VC of a UINavigationController
On button-tap, we add a "cover view" to the navigation controller's view hierarchy
We initially position that view below the bottom of the screen
Animate it up into view
Make desired changes to VC-B's view
Instantiate and Push VC-C
Do what's needed to setup the UI on VC-C
Animate the cover view down and off-screen
Remove the cover view from the hierarchy
And here's the code. Everything is done via code - even the initial Nav Controller setup - so No Storyboard needed (go to Project General Settings and delete anything in the Main Interface field).
AppDelegate.swift
//
// AppDelegate.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// instantiate a UINavigationController
let navigationController = UINavigationController();
// instantiate a NavBViewController
let vcB = NavBViewController();
// set the navigation controller's first controller
navigationController.viewControllers = [ vcB ];
self.window?.rootViewController = navigationController;
self.window?.makeKeyAndVisible()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
}
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillEnterForeground(_ application: UIApplication) {
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
}
}
ViewControllers.swift - contains CoverView, NavBViewController and NavCViewController classes
//
// ViewControllers.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
class CoverView: UIView {
let theSpinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView()
v.translatesAutoresizingMaskIntoConstraints = false
v.style = .whiteLarge
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.textColor = .white
v.text = "Please Wait"
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .blue
// add an Activity Spinner and a label
addSubview(theSpinner)
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
theSpinner.centerXAnchor.constraint(equalTo: theLabel.centerXAnchor),
theSpinner.bottomAnchor.constraint(equalTo: theLabel.topAnchor, constant: -100.0),
])
theSpinner.startAnimating()
}
}
class NavBViewController: UIViewController {
// this view will be added or removed while the "coverView" is up
let newViewToChange: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.textColor = .white
v.textAlignment = .center
v.text = "A New View"
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller B"
return v
}()
let theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Tap Me", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add a button and a label
view.addSubview(theButton)
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theLabel.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any) {
// get
// the neavigation controller's view,
if let navView = navigationController?.view {
// create a "cover view"
let coverView = CoverView()
coverView.translatesAutoresizingMaskIntoConstraints = false
// add the coverView to the neavigation controller's view
navView.addSubview(coverView)
// give it a tag so we can find it from the next view controller
coverView.tag = 9999
// create a constraint with an .identifier so we can get access to it from the next view controller
let startConstraint = coverView.topAnchor.constraint(equalTo: navView.topAnchor, constant: navView.frame.height)
startConstraint.identifier = "CoverConstraint"
// position the coverView so its top is at the bottom (hidden off-screen)
NSLayoutConstraint.activate([
startConstraint,
coverView.heightAnchor.constraint(equalTo: navView.heightAnchor, multiplier: 1.0),
coverView.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
coverView.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
])
// we need to force auto-layout to put the coverView in the proper place
navView.setNeedsLayout()
navView.layoutIfNeeded()
// change the top constraint constant to 0 (top of the neavigation controller's view)
startConstraint.constant = 0
// animate it up
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, we'll change something in this VC's UI
self.doStuff()
}))
}
}
func doStuff() -> Void {
// if newView is already there, remove it
// else, add it to the view
// this will happen *while* the coverView is showing
if newViewToChange.superview != nil {
newViewToChange.removeFromSuperview()
} else {
view.addSubview(newViewToChange)
NSLayoutConstraint.activate([
newViewToChange.bottomAnchor.constraint(equalTo: view.bottomAnchor),
newViewToChange.leadingAnchor.constraint(equalTo: view.leadingAnchor),
newViewToChange.trailingAnchor.constraint(equalTo: view.trailingAnchor),
newViewToChange.heightAnchor.constraint(equalToConstant: 80.0),
])
}
// simulate it taking a full second
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// instantiate and push the next VC
// again, this will happen *while* the coverView is showing
let vc = NavCViewController()
self.navigationController?.pushViewController(vc, animated: false)
}
}
}
class NavCViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller C"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
// add a label
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
// do whatever else needed to setup this VC
// simulate it taking 1 second to setup this view
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// get
// the neavigation controller's view,
// the view with tag 9999 (the "coverView")
// the top constraint of the coverView
if let navView = self.navigationController?.view,
let v = navView.viewWithTag(9999),
let c = (navView.constraints.first { $0.identifier == "CoverConstraint" }) {
// change the top constant of the coverView to the height of the navView
c.constant = navView.frame.height
// animate it "down"
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, remove the coverView
v.removeFromSuperview()
}))
}
}
}
}
When you run it, it will look like this:
Tapping "Tap Me" will slide-up a "cover view" and a new red view will be added (but you won't see it):
The sample has 2-seconds worth of delay, to simulate whatever your app is doing to set up its UI. After 2-seconds, the cover view will slide down:
Revealing the pushed VC-C (confirmed by the Back button on the Nav Bar).
Tapping Back takes you back to VC-B, where you see the new red view that was added:
So, by animating the position of the cover view, we emulate the use of present() and dismiss(), and allow the push to take place behind it.
I had this code in a UITableViewController and it worked perfectly.
func setupSearchBar() {
let searchBar: UISearchBar = searchController.searchBar
tableView.tableHeaderView = searchBar
let point = CGPoint(x: 0, y: searchBar.frame.size.height)
tableView.setContentOffset(point, animated: true
}
Now I'm refactoring my code to fit more of an MVC style architecture. What I did is create a UITableView in the View class:
class View: UIView {
lazy var tableView: UITableView = {
let table = UITableView()
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
func configureView() {
// tableView
addSubview(tableView)
tableView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
tableView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
tableView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
tableView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true
}
}
and then use the View class in my ViewController:
class ViewController: UIViewController {
var newView: View! { return self.view as! View }
override func loadView() {
view = View(frame: UIScreen.main.bounds)
newView.configureView()
}
override func viewDidLoad() {
super.viewDidLoad()
setupSearchBar()
}
func setupSearchBar() {
let searchBar: UISearchBar = searchController.searchBar
newView.tableView.tableHeaderView = searchBar
let point = CGPoint(x: 0, y: searchBar.frame.size.height)
newView.tableView.setContentOffset(point, animated: true)
}
The tableView shows up no problem and everything else is fine. The only thing that's not working is the setContentOffset is being called, but it's not offsetting the content. I want the searchbar to be hidden by default when the user first opens this viewController (similar to iMessage), but after I moved the code from a UITableViewController to separate files (UIView + UIViewController) like in this example, the searchbar always shows by default.
I'm not sure why it's not working. Any help would be greatly appreciated.
It's probably a timing problem relative to layout. Instead of calling setUpSearchBar in viewDidLoad, do it later, in viewDidLayoutSubviews, when initial layout has actually taken place. This method can be called many times, so use a flag to prevent it from being called more than once:
var didSetUp = false
override func viewDidLayoutSubviews() {
if !didSetUp {
didSetUp = true
setUpSearchBar()
}
}
Also: Your animated value is wrong:
newView.tableView.setContentOffset(point, animated: true)
You mean false. You don't want this movement to be visible. The table view should just appear with the search bar out of sight.
I'm having a really frustrating problem that I'm sure has a simple solution but for the life of me, I can't figure it out.
I have a UITableView within a UIViewController. On the toolbar, I have a button that can show/hide a Search Bar. Everything works great except for the annoying fact that the search bar, upon selection, shifts up 8 pixels (the original margin between the UITableView and the SuperView) and expands in width to equal the full superview.
I have kind of fixed the width issue with the function searchBarFrame(), however, it cuts the "Cancel" button in half, so it isn't perfect (See Below). I'd really appreciate any thoughts on these two problems. I have tried every combination of Extend Edges and Scroll View Insets based on other solutions I've found, but nothing is working for me. I really don't want to use the navigation bar as the search bar nor do I want to convert completely to a UITableViewController. There must be a way to make this work!
Here is my (relevant?) code:
class ListVC: UIViewController UISearchControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBtn: UIBarButtonItem!
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
func searchBarFrame() {
var searchBarFrame = searchController.searchBar.frame
searchBarFrame.size.width = tableView.frame.size.width
searchController.searchBar.frame = searchBarFrame
}
func showSearchController() {
searchController.isActive = true
searchBarFrame()
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
definesPresentationContext = true
searchController.searchBar.placeholder = "Search Places"
searchController.searchBar.roundCorners(corners: [.topLeft, .topRight, .bottomLeft, .bottomRight], radius: 5.0)
searchController.searchBar.barTintColor = UIColor.blurColor
tableView.tableHeaderView = searchController.searchBar
}
func hideSearchController() {
tableView.tableHeaderView = nil
searchController.isActive = false
}
#IBAction func onSearchBtnPress(sender: UIBarButtonItem) {
if !searchController.isActive {
showSearchController()
} else {
hideSearchController()
}
}
Following up, in case anyone else experienced this issue. After a lot of time and effort, I never got my original setup to work. Instead, I started from scratch and approached it differently.
In storyboard (you can do this programmatically too but I went the easier route because I was fed up), I put a UISearchBar inside a UIView inside a UIStackView. I set the Stackview's leading and trailing constraints to the uitableview, the bottom to the top of the uitableview and the top to the bottom of the top layout view. The UIView's only constraint is a height of 56 (the typical search bar height) with a priority of 999 (if you want to show and hide).
This fixed everything and the code was really simple too.
class MyVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
searchView.isHidden = true
}
#IBAction func onSearchBtnPress(sender: UIBarButtonItem) {
if searchView.isHidden {
searchView.isHidden = false
} else {
searchView.isHidden = true
}
}
}
extension MyVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// do something
}
I imagine that the use of NSLayoutConstraint to position and size your views would solve this issue.
For example:
private let margin: CGFloat = 15.0
tableView.translatesAutoresizingMaskIntoConstraints = false
searchBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: margin),
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: margin),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
searchBar.widthAnchor.constraint(equalTo: tableView.widthAnchor)
])