I am creating tableView in my application programmatically, and I have this problem:
Whenever the content size of my tableView is bigger than its frame, the first row of tableView appears under the header initially, but it can be easily scrolled back into the position.
The screenshots of what the window looks like after the initial load.
Content size bigger than tableView frame(the issue is here):
Content size smaller than tableView frame(all good here):
My NSViewController class code:
class MainViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
let label: NSTextField = {
let label = NSTextField()
label.stringValue = "Test App"
label.font = NSFont.boldSystemFont(ofSize: 14)
label.textColor = .white
label.alignment = .center
label.drawsBackground = false
label.isBezeled = false
label.isSelectable = true
label.isEditable = false
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelContainerView: NSView = {
let view = NSView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let tableView: NSTableView = {
let tableView = NSTableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.usesAlternatingRowBackgroundColors = true
let column = NSTableColumn(identifier: .init(rawValue: "MyColumnID"))
column.headerCell.title = "Test"
column.headerCell.alignment = .center
tableView.addTableColumn(column)
return tableView
}()
let scrollView: NSScrollView = {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
override func loadView() {
view = NSView()
view.wantsLayer = true
}
var names: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
layoutUI()
for i in 1...9 {
let name = "Test \(i)"
names.append(name)
}
scrollView.documentView = tableView
tableView.delegate = self
tableView.dataSource = self
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let newCell = MyCellView(identfier: NSUserInterfaceItemIdentifier(rawValue: "MyColumnID"))
newCell.textView.stringValue = names[row]
return newCell
}
func numberOfRows(in tableView: NSTableView) -> Int {
return names.count
}
func layoutUI() {
view.addSubview(labelContainerView)
view.addSubview(scrollView)
labelContainerView.addSubview(label)
NSLayoutConstraint.activate([
labelContainerView.topAnchor.constraint(equalTo: view.topAnchor),
labelContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
labelContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
labelContainerView.heightAnchor.constraint(equalTo: view.heightAnchor,
multiplier: 1/5),
label.centerXAnchor.constraint(equalTo: labelContainerView.centerXAnchor),
label.centerYAnchor.constraint(equalTo: labelContainerView.centerYAnchor,
constant: -4),
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor,
multiplier: 4/5),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
I would not recommend to setup a NSTableView in code. Its much easier to do this in Interface Builder and may prevent unwanted side effects. In any case if NSTableView is not displaying the first row as expected I would use scrollToVisible on the related NSScrollView to make the first row visible after loading the data.
I've been working on the same problem (programmatically creating scrolling tables). I built a breadboard version, which worked except for this particular problem.
When an NSTableView has an NSTableHeaderView, the NSScrollview actually has TWO NSClipviews. The one accessed from the .contentView property contains the table view, the 2nd contains the header and an NSVisualEffectView. They are both subviews of the scroll view.
I got it to behave, that is, show the top row, by adjusting the contentView insets:
scrollView.contentView.contentInsets = NSEdgeInsets(top: headerView!.frame.height, left: 0, bottom: 0, right: 0)
I tried various auto layout strategies, which didn't work, or seemed dubious.
Here is a trace of what it looks like for a two-column table filled with generated data to let me see what's going on.
Overview (not code, but the only way I could get it to format):
Scroll View Subviews: 4
NSClipView
NSTableBackgroundView (-250.0, -456.0, 1000.0, 456.0)
NSTableView (0.0, 0.0, 500.0, 4750.0)
NSClipView
NSVisualEffectView (0.0, 0.0, 500.0, 23.0)
NSTableHeaderView (0.0, 0.0, 500.0, 23.0)
NSScroller
NSScroller
The two clip views, with their frame rects. 0 is the content view:
ClipView 0 (0.0, 0.0, 500.0, 438.0)
NSTableBackgroundView 0 (-250.0, -456.0, 1000.0, 456.0)
NSTableView 1 (0.0, 0.0, 500.0, 4750.0)
ClipView 1 (0.0, 0.0, 500.0, 23.0)
NSVisualEffectView 0 (0.0, 0.0, 500.0, 23.0)
NSTableHeaderView 1 (0.0, 0.0, 500.0, 23.0)
Still don't completely understand what's going on.
But I do believe that knowing how this stuff works under the hood, without IB magic, is a worthwhile exercise.
Related
My problem seems obvious and duplicated but I can't manage to make it work.
I'm trying to achieve the famous stretchy header effect (image's top side stuck to top of UIScrollView when scrolling), but with an UIPageViewController instead of simply an image.
My structure is:
UINavigationBar
|-- UIScrollView
|-- UIView (totally optional container)
|-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
|-- UITableView (not embedded but could be)
My UIPageViewController contains images to make a carousel, nothing more.
All my views are laid out with NSLayoutConstraints (with visual format for vertical layout in the container).
I trie sticking topAnchor of the page controller's view to the one of self.view (with or without priority) but no luck, and no matter what I do it changes absolutely nothing.
I finally tried to use SnapKit but it doesn't work neither (I don't know much about it but it seems to only be a wrapper for NSLayoutConstaints so I'm not surprised it doesn't work too).
I followed this tutorial, this one and that one but none of them worked.
(How) can I achieve what I want?
EDIT 1:
To clarify, my carousel currently has a forced height of 350. I want to achieve this exact effect (that is shown with a single UIImageView) on my whole carousel:
To clarify as much as possible, I want to replicate this effect to my whole UIPageViewController/carousel so that the displayed page/image can have this effect when scrolled.
NOTE: as mentioned in the structure above, I have a (transparent) navigation bar, and my safe area insets are respected (nothing goes under the status bar). I don't think it would change the solution (as the solution is probably a way to stick the top of the carousel to self.view, no matter the frame of self.view) but I prefer you to know everything.
EDIT 2:
Main VC with #DonMag's answer:
private let info: UITableView = {
let v = UITableView(frame: .zero, style: .insetGrouped)
v.backgroundColor = .systemBackground
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
private lazy var infoHeightConstraint: NSLayoutConstraint = {
// Needed constraint because else standalone UITableView gets an height of 0 even with usual constraints
// I update this constraint in viewWillAppear & viewDidAppear when the table gets a proper contentSize
info.heightAnchor.constraint(equalToConstant: 0.0)
}()
private let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
...
// MARK: Views declaration
// Container for carousel
let stretchyView = UIView()
stretchyView.translatesAutoresizingMaskIntoConstraints = false
// Carousel
let carouselController = ProfileDetailCarousel(images: [
UIImage(named: "1")!,
UIImage(named: "2")!,
UIImage(named: "3")!,
UIImage(named: "4")!
])
addChild(carouselController)
let carousel: UIView = carouselController.view
carousel.translatesAutoresizingMaskIntoConstraints = false
stretchyView.addSubview(carousel)
carouselController.didMove(toParent: self)
// Container for below-carousel views
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
// Texts and bio
let bioController = UIHostingController(rootView: ProfileDetailBio())
addChild(bioController)
let bio: UIView = bioController.view
bio.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(bio)
bioController.didMove(toParent: self)
// Info table
info.delegate = tableDelegate
info.dataSource = tableDataSource
tableDelegate.viewController = self
contentView.addSubview(info)
[stretchyView, contentView].forEach { v in
scrollView.addSubview(v)
}
view.addSubview(scrollView)
// MARK: Constraints
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.topAnchor)
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// Scroll view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
// Stretchy view
stretchyTop,
stretchyView.leadingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.leadingAnchor),
stretchyView.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor),
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 350.0),
// Carousel
carousel.topAnchor.constraint(equalTo: stretchyView.topAnchor),
carousel.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
carousel.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
carousel.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
// Content view
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 350.0),
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor),
// Bio
bio.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
bio.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
info.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
info.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
infoHeightConstraint
])
}
Your view hierarchy should be:
UINavigationBar
|-- UIScrollView
|-- UIView ("stretchy" container view)
|-- UIPageViewController (as UIView, embedded with asChild())
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
To get the stretchy view to "stick to the top":
We constrain the stretchy view's Top to the scroll view's .frameLayoutGuide Top, but we give that constraint a less-than-required .priority so we can "push it" up and off the screen.
We also give the stretchy view a Height constraint of greater-than-or-equal-to 350. This will allow it to stretch - but not compress - vertically.
We'll call the view from the UIHostingViewController our "contentView" ... and we'll constrain its Top to the stretchy view's Bottom.
Then, we give the content view another Top constraint -- this time to the scroll view's .contentLayoutGuide, with a constant of 350 (the height of the stretchy view). This, plus the Leading/Trailing/Bottom constraints defines the "scrollable area."
When we scroll (pull) down, the content view will "pull down" the Bottom of the stretchy view.
When we scroll (push) up, the content view will "push up" the entire stretchy view.
Here's how it looks (too big to add as a gif here): https://imgur.com/a/wkThhzN
And here's the sample code to make that. Everything is done via code, so no #IBOutlet or other connections needed. Also note that I used three images for the page views - "ex1", "ex2", "ex3":
View Controller
class StretchyHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let stretchyView: UIView = {
let v = UIView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let stretchyViewHeight: CGFloat = 350.0
override func viewDidLoad() {
super.viewDidLoad()
// set to a greter-than-zero value if you want spacing between the "pages"
let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
// instantiate the Page View controller
let pgVC = SamplePageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: opts)
// add it as a child controller
self.addChild(pgVC)
// safe unwrap
guard let pgv = pgVC.view else { return }
pgv.translatesAutoresizingMaskIntoConstraints = false
// add the page controller view to stretchyView
stretchyView.addSubview(pgv)
pgVC.didMove(toParent: self)
NSLayoutConstraint.activate([
// constrain page view controller's view on all 4 sides
pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
])
[scrollView, stretchyView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and stretchyView to the scroll view
[stretchyView, contentView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to self.view
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
// keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
// so its Height will "stretch" when scroll view is pulled down
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
// priority needs to be less-than-required so we can "push it up" out of view
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
// scroll view Leading/Trailing/Bottom to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain stretchy view Top to scroll view's FRAME
stretchyTop,
// stretchyView to Leading/Trailing of scroll view FRAME
stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// stretchyView Height - greater-than-or-equal-to
// so it can "stretch" vertically
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
// content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// content view Width to scroll view's FRAME
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
// content view Top to scroll view's CONTENT GUIDE
// plus Height of stretchyView
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),
// content view Top to stretchyView Bottom
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
}
func addSomeContent() {
// vertical stack view with 20 labels
// so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
stack.backgroundColor = .gray
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
stack.addArrangedSubview(v)
}
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
])
}
}
Controller for each Page
class OnePageVC: UIViewController {
var image: UIImage = UIImage() {
didSet {
imgView.image = image
}
}
let imgView: UIImageView = {
let v = UIImageView()
v.backgroundColor = .systemBlue
v.contentMode = .scaleAspectFill
v.clipsToBounds = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
}
}
Sample Page View Controller
class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var controllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
let imgNames: [String] = [
"ex1", "ex2", "ex3",
]
for i in 0..<imgNames.count {
let aViewController = OnePageVC()
if let img = UIImage(named: imgNames[i]) {
aViewController.image = img
}
self.controllers.append(aViewController)
}
self.dataSource = self
self.delegate = self
self.setViewControllers([controllers[0]], direction: .forward, animated: false)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index > 0 {
return controllers[index - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
return controllers[index + 1]
}
return nil
}
}
Edit
Looking at the code you posted in your question's Edit... it's a little tough, since I don't know what your ProfileDetailBio view is, but here are a couple tips to help debug this type of situation during development:
give your views contrasting background colors... makes it easy to see the frames when you run the app
if a subview fills its superview's width, make it a little narrower so you can see what's "behind / under" it
set .clipsToBounds = true on views you're using as "containers" - such as contentView... if a subview is then "missing" you know it has extended outside the bounds of the container
So, for your code...
// so we can see the contentView frame
contentView.backgroundColor = .systemYellow
// leave some space on the right-side of bio view, so we
// so we can see the contentView behind it
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -100.0),
If you run the app, you will likely see that contentView only extends to the bottom of bio - not to the bottom of info.
If you then do this:
contentView.clipsToBounds = true
info will likely not be visible at all.
Checking your constraints, you have:
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
where it should be:
// no bio bottom anchor
//bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// this is correct
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
// add this
info.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
Run the app, and you should now again see info, and contentView extends to the bottom of info.
Assuming bio and info height combined are tall enough to require scrolling, you can undo the "debug / dev" changes and you should be good to go.
I’m trying to get the tableView to move up when the search bar does. Take a look at the problem:
I think I see what the issue is here, but I can't think of a solution. In SearchResultsUpdating I have an animation block:
func updateSearchResults(for searchController: UISearchController) {
UIView.animateKeyframes(withDuration: 1, delay: 0, options: UIView.KeyframeAnimationOptions(rawValue: 7)) {
self.tableView.frame = CGRect(x: 20, y: self.view.safeAreaInsets.top, width:
self.view.frame.size.width-40, height: self.view.frame.size.height -
self.view.safeAreaInsets.top)
}
}
It seems to me that the animation block is only receiving the previous coordinates for the y origin, hence it is animating out of sync. I tried adding a target to the tableView, or navigationBar, or the searchBarTextField instead, but nothing worked.
Any help is appreciated, thanks!
EDIT: After implementing Shawn's second suggestion this was the result:
I can't imagine why it isn't animating smoothly now... very frustrating!
EDIT 2 - Requested Code:
class ViewController: UIViewController{
//City TableView
let cityTableView = UITableView()
let searchVC: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.placeholder = "Search"
return searchController
}()
//viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
//Do any setup for the view controller here
setupViews()
//CityViewController
setupCityViewTableView()
}
//setupViews
func setupViews(){
//NAVIGATIONBAR:
//title
title = "Weather"
//set to hidden because on initial load there is a scroll view layered over top of the CityViewTableView (code not shown here). This gets set to false when the scrollView alpha is set to 0 and the CityViewTableView is revealed
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
}
//MARK: -CityViewController Functions
extension ViewController{
//setUp TableView
func setupCityViewTableView(){
cityTableView.translatesAutoresizingMaskIntoConstraints = false
//set tableView delegate and dataSource
cityTableView.delegate = self
cityTableView.dataSource = self
//background color
cityTableView.backgroundColor = .black
//separator color
cityTableView.separatorColor = .clear
//is transparent on initial load
cityTableView.alpha = 0
//set tag
cityTableView.tag = 1000
//hide scroll indicator
cityTableView.showsVerticalScrollIndicator = false
//register generic cell
cityTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cityCell")
//add subview
view.addSubview(cityTableView)
//Auto Layout
cityTableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
cityTableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
cityTableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
//MARK: -TableView Controller
extension ViewController: UITableViewDelegate,
UITableViewDataSource{
//number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if tableView.tag == 1000{
return 5
}
return self.models[tableView.tag].count
}
//cell for row
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
//CityViewController
if tableView.tag == 1000{
let cell = tableView.dequeueReusableCell(withIdentifier:
"cityCell", for: indexPath)
cell.textLabel?.text = "Test"
cell.textLabel?.textAlignment = .center
cell.backgroundColor = .systemGray
cell.selectionStyle = .none
cell.layer.cornerRadius = 30
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 5
cell.layer.cornerCurve = .continuous
return cell
}
//WeatherViewController
//code here for scrollView tableViews
}
//Height for row
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableView.tag == 1000{
return view.frame.size.height/7
}
return view.frame.size.height/10
}
//Should Highlight Row
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if tableView.tag == 1000{
return true
}
return false
}
//Did select row
func tableView(_ tableView: UITableView, didSelectRowAt
indexPath: IndexPath) {
//calls function for segue to Weather Scroll View (not shown)
if tableView.tag == 1000{
segueToWeatherView(indexPath: indexPath)
}
}
}
EDIT 3: When I comment out another function it finally works, but I'm not sure exactly why, or how to fix it. This is the function in question, addSubViews()
//setup viewController
func addSubViews(){
//add weatherView as subView of ViewController
view.addSubview(weatherView)
//add subviews to weatherView
weatherView.addSubview(scrollView)
weatherView.addSubview(pageControl)
weatherView.addSubview(segueToCityViewButton)
weatherView.addSubview(segueToMapViewButton)
}
Specifically, it works when I comment out this line:
view.addSubview(weatherView)
Here is all the code concerning the setting up of the weatherView and all of its subViews:
//Any additional setup goes here
private func setupViews(){
//VIEWCONTROLLER:
//title
title = "Weather"
//Background color of view Controller
view.backgroundColor = .darkGray
//WEATHERVIEW:
//Background color of weather view Controller
weatherView.backgroundColor = .clear
//weatherView frame
weatherView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//SCROLLVIEW:
//background color of scroll view
scrollView.backgroundColor = .clear
//scrollView frame
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//changed
//PAGECONTROL:
//page control frame
pageControl.frame = CGRect(x: 0, y: view.frame.height-view.frame.size.height/14, width: view.frame.width, height: view.frame.size.height/14)
//TRANSITIONVIEW:
//TransitionView frame
transitionView.frame = CGRect(x: 20, y: 0, width: view.frame.size.width-40, height: view.frame.size.height)
//BUTTONS:
//segue to CityView
segueToCityViewButton.frame = CGRect(x: (weatherView.frame.width/5*4)-20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//segue to MapView:
segueToMapViewButton.frame = CGRect(x: 20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//LABELS:
transitionViewLabel.frame = transitionView.bounds
//NAVIGATIONBAR:
//set to hidden on initial load
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
For the sake of being thorough, here is the full viewDidLoad() Function:
override func viewDidLoad() {
super.viewDidLoad()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
//MARK: CityViewController
setupCityViewTableViews()
}
EDIT 4: With the following changes in viewDidLoad(), I finally got it to work!
override func viewDidLoad() {
super.viewDidLoad()
//MARK: CityViewController
//Moved to a position before setting up the other views
setupCityViewTableViews()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
}
Doing it the way you are doing it right now is a way to do it but I think it is the most challenging way to do it for several reasons:
You don't have much control and access to the implementation of the search controller animation within the navigation bar so getting the right coordinates might be a task
Even if you did manage to get the right coordinates, trying to synchronize your animation frames and timing to look in sync and seamless with the search animation on the nav bar will be tricky
I suggest the 2 following alternatives to what you are currently doing where you will get the news experience pretty much for free out of the box.
Option 1: Use a UITableViewController instead of a UIViewController
This is all the code using a UITableViewController and adding a UISearchController to the navigation bar.
class NewsTableViewVC: UITableViewController
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .black
title = "Weather"
// Ignore this as you have you own custom cell class
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
setUpNavigationBar()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
}
This is the experience you can expect
Option 2: Use auto layouts rather than frames to configure your UITableView
If you don't want to use a UITableViewController, configure your UITableView using auto layout rather than frames which has a little more work but not too much:
class NewsTableViewVC: UIViewController, UITableViewDataSource, UITableViewDelegate
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
private let tableView = UITableView()
override func viewDidLoad()
{
super.viewDidLoad()
// Just to show it's different from the first
view.backgroundColor = .purple
title = "Weather"
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
private func setUpTableView()
{
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = .clear
view.addSubview(tableView)
// Auto Layout
tableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 0).isActive = true
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
tableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
tableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: 0).isActive = true
tableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
You can expect the following experience:
Update
This is based on your updated code, you missed one small detail which might be impacting the results you see and this is the top constraint of the UITableView.
You added the constraint to the safeAreaLayoutGuide top anchor:
cityTableView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 0).isActive = true
My recommendation from the code above if you notice is to add it to the view top constraint
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
Give this a go and see if you come close to getting what you expect ?
Here is a link to the complete code of my implementation if it helps:
I have a ViewController named MainViewController that acts as the central page for the app. This MainViewController has 6 properties in it that are instances of other viewControllers
lazy var shelvesView: ShelvesViewController = {
return ShelvesViewController()
}()
lazy var goalsView: GoalsViewController = {
return GoalsViewController()
}()
lazy var shoppingView: ShoppingListViewController = {
return ShoppingListViewController()
}()
lazy var tipsView: TipsViewController = {
return TipsViewController()
}()
lazy var myDenView: MyDenViewController = {
return MyDenViewController()
}()
lazy var settingsview: SettingsViewController = {
return SettingsViewController()
}()
when the mainViewController loads it starts out with the shelvesViewController laid over it just underneath the mainViewControllers custom NavBar like so
func setupShelvesView() {
shelvesView.willMove(toParentViewController: self)
addChildViewController(shelvesView)
self.view.addSubview(shelvesView.view)
shelvesView.view.frame = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
shelvesView.didMove(toParentViewController: self)
globalCurrentView = 1
}
I also have a menu that slides over this mainViewController with a list of different pages the user can navigate to. when the user switches to a new page, say the goalsViewController, the shelvesViewController will be animated off screen, removed from the parentViewController(MainViewController) and the goalsViewController will be initialized, moved to the MainViewController and animated onscreen in the same frame as the shelvesView. Anytime i switch a VC from the menu i use this method.
func changeVCfrom(OldVC oldVC: UIViewController, newVC: UIViewController) {
let newStartFrame = CGRect(x: 0 + self.view.frame.width, y: 0, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let newEndframe = CGRect(x: 0, y: view.frame.height * 0.08, width: view.frame.width, height: view.frame.height - (view.frame.height * 0.08))
let oldfinishFrame = CGRect(x: 0 - self.view.frame.width, y: 0, width: self.view.frame.width, height: self.view.frame.height)
oldVC.willMove(toParentViewController: nil)
self.addChildViewController(newVC)
newVC.view.frame = newStartFrame
transition(from: oldVC, to: newVC, duration: 0.2, options: [.curveEaseOut], animations: {
oldVC.view.frame = oldfinishFrame
newVC.view.frame = newEndframe
}, completion: { (success) in
oldVC.willMove(toParentViewController: nil)
oldVC.view.removeFromSuperview()
oldVC.removeFromParentViewController()
newVC.willMove(toParentViewController: self)
self.view.addSubview(newVC.view)
})
}
the problem here is that i have done all my views programmatically and any of the above viewControllers(lazy properties) that have a tableView propery on them have a memory leak. any time the VC goes off the MainViewController the memory used for the tableView is not being dealocated and anytime it comes back on the MainViewController is is being allocated again for more memory.
as of right now this is how ive been setting up my tableViews
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
setupObjects()
tableView.register(ShelfTableViewCell.self, forCellReuseIdentifier: "shelfCell")
}
func setupTableView() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
setupTableViewConstraints()
}
func setupTableViewConstraints() {
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
I understand that I need to have my tableviews either weak, unowned or somehow get the tableview to completely deallocate when off screen. but whatever i try I get errors all over the place that i cant resolve or my ViewController is not able to load the tableview. I apologize for the lengthy question, but any help on this would be very much appreciated.
The table views are not getting deallocated when your child view controllers go off screen.
Your MainViewController owns those other view controllers. Yes, they're lazy, but once they're called for the first time they are initialized along with the tableviews they own.
The child view controllers are not deallocated by simply moving them offscreen. All you're doing is changing the frame and removing them as a child view controller of MainViewController. Since the they're still in memory, the table view is also still in memory.
If you really want to deallocate the table view every time the controller moves off screen, you can just set
self.tableView = nil
There shouldn't be a retain cycle here because the tableView's datasource and delegate properties are weak, meaning that they don't increase the reference count of the view controller.
while scrolling up and down on the tableView my memory would go up sharply. Turns out i had set up the dequeReusableCell wrong.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}
essentially what was happening was every time the viewWillAppear was hit i reloaded the tableViewData which ran this code
let cell = ShelfTableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "shelfCell")
tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath)
which was causing the memory leak
replacing the above code with the following resolved the issue
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "shelfCell", for: indexPath) as! ShelfTableViewCell
if let shelf = UserController.shared.user?.shelves?[indexPath.row] as? Shelf {
cell.shelf = shelf
return cell
} else {
return UITableViewCell()
}
}
I have a UIView that I'm appending into a stack view on my main page of my app
This is the class of my view:
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
leftLabel.topAnchor.constraint(equalTo: topAnchor),
leftLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
rightLabel.topAnchor.constraint(equalTo: topAnchor),
rightLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
}
And I append to my main stack view with:
let myCustomView = MyCustomView(frame: .zero)
stackView.addArrangedSubview(myCustomView)
This loads in my label's correctly and resizes everything as I'd want.
However, in my main class, I am updating myCustomView.rightLabel.text = <New Way Longer Text That Takes 2 Lines Instead of One>
The text is updating properly, but my myCustomView size is not resizing, so part of the text is just being cut-off
I have tried following other answers on here, but none of them seem to work for me.
Am I missing something small in order to force the resize of the customView to fit the label inside of it?
Thank you in advance
Your code does not show that you set the .numberOfLines in the label(s) to 0 to allow for multi-line labels.
Adding only that, should allow your labels to grow in height and to expand your custom view. However... that will also make both labels expand to the size of the tallest label, resulting in the text of the "shorter" label being vertically centered (I added background colors to make it easy to see the frames / bounds of the views):
If you constrain the Bottom of your custom view to the Bottom of each label at greaterThanOrEqualTo you can keep the labels "top-aligned":
You can run this code directly in a Playground page to see the results:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyCustomView: UIView {
public let leftLabel: UILabel = UILabel(frame: .zero)
public let rightLabel: UILabel = UILabel(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(leftLabel)
addSubview(rightLabel)
// background colors so we can see the view frames
backgroundColor = .cyan
leftLabel.backgroundColor = .yellow
rightLabel.backgroundColor = .green
// we want multi-line labels
leftLabel.numberOfLines = 0
rightLabel.numberOfLines = 0
// use auto-layout
leftLabel.translatesAutoresizingMaskIntoConstraints = false
rightLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain to top
leftLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to left
leftLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
// constrain width = 40%
leftLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.4),
// constrain to top
rightLabel.topAnchor.constraint(equalTo: topAnchor),
// constrain to right
rightLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
// constrain width = 60%
rightLabel.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6),
// constrain bottom of view (self) to >= 0 from bottom of leftLabel
bottomAnchor.constraint(greaterThanOrEqualTo: leftLabel.bottomAnchor, constant: 0.0),
// constrain bottom of view (self) to >= 0 from bottom of rightLabel
bottomAnchor.constraint(greaterThanOrEqualTo: rightLabel.bottomAnchor, constant: 0.0),
])
leftLabel.text = "Short string"
rightLabel.text = "Short string too"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyViewController : UIViewController {
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var theStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
v.distribution = .equalSpacing
return v
}()
var myView = MyCustomView()
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
myView.leftLabel.text = "Short string with\nA\nB\nC\nD\nE"
myView.rightLabel.text = "Short string too\nA\nB"
}
override func loadView() {
let view = UIView()
self.view = view
view.backgroundColor = .white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
view.addSubview(theStackView)
// constrain stack view to Top: 100 and Leading/Trailing" 0
// no Bottom or Height constraint
NSLayoutConstraint.activate([
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
theStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
])
theStackView.addArrangedSubview(myView)
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I'm resizing the view that a textview belongs to and the text shakes when the view either gets bigger or gets smaller.
Declaration of said text view:
lazy var textview: UITextView = {
let textView = UITextView()
textView.text = ""
textView.font = .systemFont(ofSize: 12, weight: UIFontWeightMedium)
textView.isScrollEnabled = false
textView.isEditable = false
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.translatesAutoresizingMaskIntoConstraints = false
textView.textAlignment = .center
textView.textColor = .lightGray
textView.dataDetectorTypes = .link
return textView
}()
I'm resizing the view that it's in to fit the full screen like this
if let window = UIApplication.shared.keyWindow {
let statusBarHeight = UIApplication.shared.statusBarFrame.size.height
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveLinear, animations: {
self.frame = CGRect(x: 0, y: statusBarHeight, width: window.frame.width, height: window.frame.height - statusBarHeight)
self.layer.cornerRadius = 0
self.layoutIfNeeded()
}, completion: nil)
}
Upon doing so, the view expands perfectly but the textviews text does a bounce effect that makes the animation look extremely unprofessional... any advice?
Edit: It seems like when I remove the center text alignment option it works fine. How do I make it work with the text center aligned?
edit: I took another look at this and attempted to use the technique based in UIScrollView animation of height and contentOffset "jumps" content from bottom.
Here's a minimal working example with text view with centered text alignment which is working for me!
I'd recommend managing animations either to be all constraint based, or all frame based. I attempted a version where the animation is driven by updating the container view frame but it was starting to take too long to left it at this constraint based approach.
Hope this points you in the right direction :)
import UIKit
class ViewController: UIViewController {
lazy var textView: UITextView = {
let textView = UITextView()
textView.text = "testing text view"
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var widthConstraint: NSLayoutConstraint!
var topAnchor: NSLayoutConstraint!
override func viewDidLoad() {
view.backgroundColor = .groupTableViewBackground
// add container view and constraints
view.addSubview(containerView)
containerView.frame = view.bounds.insetBy(dx: 100, dy: 200)
containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 100).isActive = true
// keep reference to topAnchor and width as properties to animate
topAnchor = containerView.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: 100)
widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 300)
topAnchor.isActive = true
widthConstraint.isActive = true
// add text view to container view and set constraints
containerView.addSubview(textView)
textView.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
textView.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
textView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
textView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
}
#IBAction func toggleResize(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
view.layoutIfNeeded()
widthConstraint.constant = sender.isSelected ? view.bounds.width : 300
topAnchor.constant = sender.isSelected ? 20 : 100
// caculate the textView content offset for starting position based on
// expected end position at end of the animation
let xOffset = (textView.bounds.width - widthConstraint.constant) / 2
textView.contentOffset = CGPoint(x: -xOffset, y: textView.contentOffset.y)
UIView.animate(withDuration: 1) {
self.view.layoutIfNeeded()
}
}
}