Error Thread 1: EXC_BAD_ACCESS (code=1, address=0x30) - swift

Please tell me the application crashes with an error:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x30)
when clicking on the button for reminders for a table cell (switching to a new View Controller).
The error appears in the class when creating a UILabel in the line let label = UILabel ():
let oneLabel: UILabel = {
let label = UILabel()
if (UIDevice.current.userInterfaceIdiom == .pad) {
label.font = UIFont.systemFont(ofSize: 32, weight: .semibold)
label.numberOfLines = 4
}
else {
label.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
label.numberOfLines = 1
}
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Here is the code from another class that transitions to the View Controller, for which the UILabel is created:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let alarm = UIContextualAction(
style: .normal,
title: "",
handler: {(_, _, completion) in
self.notificationCenter.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
guard granted else { return DispatchQueue.main.async { self.createAlertForNotifications() } }
self.notificationCenter.getNotificationSettings { (settings) in
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
let vc = OneViewController()
self.parentController!.navigationController?.setViewControllers([vc], animated: false)
}
completion(true)
}
}
}
)
alarm.image = ListImages.alarmImage
alarm.backgroundColor = .systemPurple
return UISwipeActionsConfiguration(actions: [alarm])
}
Sometimes this error appears, sometimes not, and it may appear for the second UILabel, which is created after the first. How can you fix this error?

Sometimes I get error in let dataPicker = UIDatePicker()
Here you go to the OneViewController:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let oneObject = isFiltering ? filteredObjects[indexPath.row] : listObjects[indexPath.row]
let alarm = UIContextualAction(
style: .normal,
title: "",
handler: {(_, _, completion) in
self.notificationCenter.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
guard granted else { return DispatchQueue.main.async { self.createAlertForNotifications() } }
self.notificationCenter.getNotificationSettings { (settings) in
guard settings.authorizationStatus == .authorized else { return }
DispatchQueue.main.async {
let vc = OneViewController()
ListNameLabel.oneText = oneObject.name
self.parentController!.navigationController?.setViewControllers([vc], animated: false)
}
completion(true)
}
}
}
)
alarm.image = ListImages.alarmImage
alarm.backgroundColor = .systemPurple
return UISwipeActionsConfiguration(actions: [alarm])
}
This is class with UILabel:
import UIKit
class MainView: UIView {
let oneLabel: UILabel = {
let label = UILabel()
if (UIDevice.current.userInterfaceIdiom == .pad) {
label.font = UIFont.systemFont(ofSize: 32, weight: .semibold)
label.numberOfLines = 4
}
else {
label.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
label.numberOfLines = 1
}
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let sheduleTimeDataPicker: UIDatePicker = {
let dataPicker = UIDatePicker()
if #available(iOS 14, *) {
dataPicker.preferredDatePickerStyle = .inline
}
else {
dataPicker.preferredDatePickerStyle = .compact
}
dataPicker.datePickerMode = .dateAndTime
dataPicker.date = Date()
dataPicker.minimumDate = Date()
dataPicker.translatesAutoresizingMaskIntoConstraints = false
return dataPicker
}()
let oneView: UIView = {
let view = UIView()
view.backgroundColor = .systemGray5
view.layer.cornerRadius = 15
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(oneView)
oneView.addSubview(oneLabel)
oneView.addSubview(sheduleTimeDataPicker)
if (UIDevice.current.userInterfaceIdiom == .pad) {
// oneLabel constraints
oneLabel.leadingAnchor.constraint(equalTo: oneView.leadingAnchor, constant: 20).isActive = true
oneLabel.trailingAnchor.constraint(equalTo: oneView.trailingAnchor, constant: -20).isActive = true
oneLabel.topAnchor.constraint(equalTo: oneView.topAnchor, constant: 20).isActive = true
oneLabel.heightAnchor.constraint(equalToConstant: 120).isActive = true
// sheduleTimeDataPicker constraints
sheduleTimeDataPicker.centerXAnchor.constraint(equalTo: oneView.centerXAnchor).isActive = true
sheduleTimeDataPicker.topAnchor.constraint(equalTo: oneLabel.bottomAnchor).isActive = true
}
else {
// oneLabel constraints
oneLabel.leadingAnchor.constraint(equalTo: oneView.leadingAnchor, constant: 10).isActive = true
oneLabel.trailingAnchor.constraint(equalTo: oneView.trailingAnchor, constant: -10).isActive = true
oneLabel.topAnchor.constraint(equalTo: oneView.topAnchor, constant: 5).isActive = true
oneLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
// sheduleTimeDataPicker constraints
sheduleTimeDataPicker.leadingAnchor.constraint(equalTo: oneView.leadingAnchor, constant: 10).isActive = true
sheduleTimeDataPicker.trailingAnchor.constraint(equalTo: oneView.trailingAnchor, constant: -10).isActive = true
sheduleTimeDataPicker.topAnchor.constraint(equalTo: oneLabel.bottomAnchor).isActive = true
}
// oneView constraints
oneView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10).isActive = true
oneView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10).isActive = true
oneView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
oneView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10).isActive = true
}
func setContentView(content: OneModel) {
self.oneLabel.text = content.oneName
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This is class OneModel:
import UIKit
struct ListNameLabel {
static var oneText = ""
}
struct OneModel {
var oneName: String
static func fetchView() -> OneModel {
return OneModel(oneName: ListNameLabel.oneText)
}
}
This is class OneViewController:
import UIKit
import UserNotifications
class OneViewController: UIViewController {
var mainView = MainView()
let notificationCenter = UNUserNotificationCenter.current()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupNavigationBar()
setupView()
}
// MARK: NavigationBar
private func setupNavigationBar() {
let backBarButtonItem = UIBarButtonItem()
backBarButtonItem.image = ListImages.chevronImage
backBarButtonItem.action = #selector(backBarButtonItemTapped)
backBarButtonItem.target = self
navigationItem.leftBarButtonItem = backBarButtonItem
navigationItem.title = ""
}
// MARK: View
private func setupView() {
view.backgroundColor = .systemBackground
view.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
let guide = self.view.safeAreaLayoutGuide
// mainView constraints
mainView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
mainView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
mainView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
mainView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
// fetch
mainView.setContentView(content: OneModel.fetchView())
}
}
// MARK: Back
extension OneViewController {
#objc func backBarButtonItemTapped() {
let vc = TwoViewController()
self.navigationController?.setViewControllers([vc], animated: false)
}
}
// MARK: UNUserNotificationCenterDelegate
extension OneViewController: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound])
print(#function)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
print(#function)
}
}

Related

change height of TableViewHeader and included UILabel after button click

When I want to set a new long status by pressing the button, my profileStatusLabel height and TableViewHeader height as well don't change.
P.S. sorry about my English, if there are some mistakes
**ProfileHeaderView: UIView
**
import UIKit
...
private lazy var profileStatusLabel: UILabel = {
let profileStatusLabel = UILabel()
profileStatusLabel.numberOfLines = 0
profileStatusLabel.text = "Looking for a big, young, good looking, able to cook female gorilla"
profileStatusLabel.textColor = .gray
profileStatusLabel.font = profileNameLabel.font.withSize(14)
profileStatusLabel.textAlignment = .left
profileStatusLabel.sizeToFit()
profileStatusLabel.translatesAutoresizingMaskIntoConstraints = false
return profileStatusLabel
}()
private lazy var setStatusButton: UIButton = {
let setStatusButton = UIButton()
setStatusButton.backgroundColor = .systemBlue
setStatusButton.layer.cornerRadius = 4
setStatusButton.layer.shadowOffset = CGSize(width: 4, height: 4)
setStatusButton.layer.shadowOpacity = 0.7
setStatusButton.layer.shadowRadius = 4
setStatusButton.layer.shadowColor = UIColor.black.cgColor
setStatusButton.setTitle("Set status", for: .normal)
setStatusButton.setTitleColor(.white, for: .normal)
setStatusButton.titleLabel?.font = setStatusButton.titleLabel?.font.withSize(14)
setStatusButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
setStatusButton.translatesAutoresizingMaskIntoConstraints = false
return setStatusButton
}()
private lazy var statusText: String = {
return statusText
}()
private func setupView() {
addSubview(profileImageView)
addSubview(profileNameLabel)
addSubview(profileStatusLabel)
addSubview(statusTextField)
addSubview(setStatusButton)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
profileNameLabel.topAnchor.constraint(equalTo: topAnchor, constant: 27),
profileNameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
profileNameLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
profileNameLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 30),
profileStatusLabel.topAnchor.constraint(equalTo: profileNameLabel.bottomAnchor, constant: 10),
profileStatusLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
profileStatusLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
profileStatusLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 49),
statusTextField.topAnchor.constraint(equalTo: profileStatusLabel.bottomAnchor, constant: 10),
statusTextField.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
statusTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
statusTextField.heightAnchor.constraint(equalToConstant: 40),
setStatusButton.topAnchor.constraint(equalTo: statusTextField.bottomAnchor, constant: 16),
setStatusButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
setStatusButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
setStatusButton.heightAnchor.constraint(equalToConstant: 50),
bottomAnchor.constraint(equalTo: setStatusButton.bottomAnchor, constant: 16),
])
}
#objc private func statusTextChanged(_ textField: UITextField) {
statusText = statusTextField.text!
}
#objc private func buttonAction() {
guard statusTextField.text != nil else {
print("Text the status before press the button")
return
}
profileStatusLabel.text = statusText
profileStatusLabel.updateConstraintsIfNeeded()
self.setNeedsUpdateConstraints()
self.layoutIfNeeded()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemGray4
setupView()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
**MyCustomHeader: UITableViewHeaderFooterView
**
import UIKit
class MyCustomHeader: UITableViewHeaderFooterView {
var profileHeaderView = ProfileHeaderView()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
configureContents()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureContents() {
contentView.addSubview(profileHeaderView)
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: profileHeaderView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: profileHeaderView.trailingAnchor),
contentView.widthAnchor.constraint(equalTo: profileHeaderView.widthAnchor),
contentView.heightAnchor.constraint(equalTo: profileHeaderView.heightAnchor),
contentView.topAnchor.constraint(equalTo: profileHeaderView.topAnchor),
])
}
}
**ProfileViewController
**
import UIKit
class ProfileViewController: UIViewController, UIGestureRecognizerDelegate {
let postList = [robberyPost, eatingPost, elephantPost, camelPost]
var profileTableView = UITableView()
let myCustomHeader = MyCustomHeader()
let headerID = "headerId"
let headerID2 = "headerId2"
let cellID = "cellId"
let collectionCellID = "collectionCellId"
...
func setupTableView() {
profileTableView.contentInsetAdjustmentBehavior = .never
profileTableView.register(PhotosTableViewCell.self, forCellReuseIdentifier: PhotosTableViewCell.cellID)
profileTableView.register(PostTableViewCell.self, forCellReuseIdentifier: PostTableViewCell.cellID)
profileTableView.delegate = self
profileTableView.dataSource = self
profileTableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(profileTableView)
}
func setupMyCustomHeader() {
profileTableView.register(MyCustomHeader.self, forHeaderFooterViewReuseIdentifier: headerID)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
profileTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
profileTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
profileTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
profileTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
...
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGray6
setupTableView()
setupMyCustomHeader()
setupConstraints()
}
}
extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
// MARK: - Table view data source
...
// MARK: - Table view delegate
...
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if #available(iOS 15, *) {
tableView.sectionHeaderTopPadding = 0
}
if section == 0 {
return UITableView.automaticDimension
} else {
return 50
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! MyCustomHeader
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(profileImageViewClicked(_ :)))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.numberOfTouchesRequired = 1
tapRecognizer.delegate = self
header.profileHeaderView.profileImageView.isUserInteractionEnabled = true
header.profileHeaderView.profileImageView.addGestureRecognizer(tapRecognizer)
return header
} else {
let header2 = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID2) as! OneMoreCustomHeader
return header2
}
}
...
}
Probably I should use updateConstraints() function, but can't find the right way.
I added this code to buttonAction, buy it doesn't work
profileStatusLabel.updateConstraintsIfNeeded()
self.setNeedsUpdateConstraints()
self.layoutIfNeeded()
Any time you change the height of a table view element - cell, section header or footer view, etc - you must tell the table view to re-layout its elements.
This is commonly done with a closure.
For example, you would add this property to your custom header view class:
// closure so table layout can be updated
var contentChanged: (() -> ())?
and then set that closure in viewForHeaderInSection:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! MyCustomHeader
// set the closure
header.contentChanged = {
// tell the table view to re-layout itself so the header height can be changed
tableView.performBatchUpdates(nil)
}
return header
} else {
let header2 = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID2) as! OneMoreCustomHeader
return header2
}
}
and when you tap the "Set status" button to update the profileStatusLabel, you would use the closure to "call back" to the controller:
contentChanged?()
With the code you posted, you are embedding ProfileHeaderView in MyCustomHeader, which complicates things a little because you will need a closure in MyCustomHeader that works with another closure in ProfileHeaderView.
There really is no need to do that -- you can put all of your UI elements directly in MyCustomHeader to avoid that issue.
Here is a complete, runnable example. I changed your MyCustomHeader as described... as well as added some other code that you will probably end up needing (see the comments):
class MyCustomHeader: UITableViewHeaderFooterView {
// closure to inform the controller the status text changed
// so we can update the data and
// so the table layout can be updated
var contentChanged: ((String) -> ())?
// presumably, we'll be setting the statusText and the name from a dataSource
public var statusText: String = "" {
didSet {
profileStatusLabel.text = statusText
}
}
public var name: String = "" {
didSet {
profileNameLabel.text = name
}
}
private lazy var profileImageView: UIImageView = {
let v = UIImageView()
if let img = UIImage(systemName: "person.crop.circle") {
v.image = img
}
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
private lazy var profileNameLabel: UILabel = {
let profileStatusLabel = UILabel()
profileStatusLabel.numberOfLines = 0
profileStatusLabel.text = ""
profileStatusLabel.textColor = .black
profileStatusLabel.textAlignment = .left
profileStatusLabel.translatesAutoresizingMaskIntoConstraints = false
return profileStatusLabel
}()
private lazy var profileStatusLabel: UILabel = {
let profileStatusLabel = UILabel()
profileStatusLabel.numberOfLines = 0
profileStatusLabel.text = ""
profileStatusLabel.textColor = .gray
profileStatusLabel.textAlignment = .left
profileStatusLabel.translatesAutoresizingMaskIntoConstraints = false
return profileStatusLabel
}()
private lazy var setStatusButton: UIButton = {
let setStatusButton = UIButton()
setStatusButton.backgroundColor = .systemBlue
setStatusButton.layer.cornerRadius = 4
setStatusButton.layer.shadowOffset = CGSize(width: 4, height: 4)
setStatusButton.layer.shadowOpacity = 0.7
setStatusButton.layer.shadowRadius = 4
setStatusButton.layer.shadowColor = UIColor.black.cgColor
setStatusButton.setTitle("Set status", for: .normal)
setStatusButton.setTitleColor(.white, for: .normal)
setStatusButton.setTitleColor(.lightGray, for: .highlighted)
setStatusButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
setStatusButton.translatesAutoresizingMaskIntoConstraints = false
return setStatusButton
}()
private lazy var statusTextField: UITextField = {
let statusTextField = UITextField()
statusTextField.text = ""
statusTextField.borderStyle = .roundedRect
statusTextField.backgroundColor = .white
statusTextField.translatesAutoresizingMaskIntoConstraints = false
return statusTextField
}()
private func setupView() {
contentView.addSubview(profileImageView)
contentView.addSubview(profileNameLabel)
contentView.addSubview(profileStatusLabel)
contentView.addSubview(statusTextField)
contentView.addSubview(setStatusButton)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
profileImageView.widthAnchor.constraint(equalToConstant: 160.0),
profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),
profileNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 27),
profileNameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
profileNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
profileNameLabel.heightAnchor.constraint(equalToConstant: 30),
profileStatusLabel.topAnchor.constraint(equalTo: profileNameLabel.bottomAnchor, constant: 10),
profileStatusLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
profileStatusLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
profileStatusLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 49),
statusTextField.topAnchor.constraint(equalTo: profileStatusLabel.bottomAnchor, constant: 10),
statusTextField.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 20),
statusTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
statusTextField.heightAnchor.constraint(equalToConstant: 40),
setStatusButton.topAnchor.constraint(equalTo: statusTextField.bottomAnchor, constant: 16),
setStatusButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
setStatusButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
setStatusButton.heightAnchor.constraint(equalToConstant: 50),
contentView.bottomAnchor.constraint(equalTo: setStatusButton.bottomAnchor, constant: 16),
])
}
#objc private func buttonAction() {
guard let stText = statusTextField.text else {
print("Text the status before press the button")
return
}
statusTextField.resignFirstResponder()
// update statusText property
// which will also set the text in the label
statusText = stText
// call the closure, passing back the new text
contentChanged?(stText)
}
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = .systemGray4
setupView()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ProfileViewController: UIViewController, UIGestureRecognizerDelegate {
// presumably, this will be loaded from saved data, along with the rest of the table data
var dataSourceStatusText: String = "Looking for a big, young, good looking, able to cook female gorilla"
var profileTableView = UITableView()
let headerID = "headerId"
let cellID = "cellId"
func setupTableView() {
profileTableView.contentInsetAdjustmentBehavior = .never
profileTableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
profileTableView.delegate = self
profileTableView.dataSource = self
profileTableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(profileTableView)
}
func setupMyCustomHeader() {
profileTableView.register(MyCustomHeader.self, forHeaderFooterViewReuseIdentifier: headerID)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
profileTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
profileTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
profileTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
profileTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGray6
setupTableView()
setupMyCustomHeader()
setupConstraints()
}
}
extension ProfileViewController: UITableViewDelegate, UITableViewDataSource {
// let's use 10 sections each with 5 rows so we can scroll the header out-of-view
func numberOfSections(in tableView: UITableView) -> Int {
return 10
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if #available(iOS 15, *) {
tableView.sectionHeaderTopPadding = 0
}
if section == 0 {
return UITableView.automaticDimension
} else {
return 50
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! MyCustomHeader
header.name = "#KillaGorilla"
header.statusText = dataSourceStatusText
// set the closure
header.contentChanged = { [weak self] newStatus in
guard let self = self else { return }
// update the status text (probably also saving it somewhere?)
// if we don't do this, and the section header scrolls out of view,
// the *original* status text will be shown
self.dataSourceStatusText = newStatus
// tell the table view to re-layout itself so the header height can be changed
tableView.performBatchUpdates(nil)
}
return header
} else {
// this would be your other section header view
// for now, let's just use a label
let v = UILabel()
v.backgroundColor = .yellow
v.text = "Section Header: \(section)"
return v
}
}
}
Give that a try.

delegate not being called in Network class

I am working with stackoverflow api in Worker class I am getting data and adding to tableView. I managed to connect and get the data and I'm trying to add it to the main view controller using a delegate, but the compiler ignores this line, what could be the matter?
When writing to the searchButton procedure, I call a method of the Worker class that receives data from the API and adds it to the array. Then, with the help of a delegate, I return a mass, but this delegate is not called.
class viewController
import UIKit
protocol WorkerDelegate: AnyObject {
func getListModels(noteModels: [CellModel])
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, WorkerDelegate{
private var listModels: [CellModel] = []
private var searchText = UITextField()
private var searchButton = UIButton()
private var table = UITableView()
private let worker = Worker()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let worker = Worker()
worker.workerDelegate = self
setupSearchText()
setupSearchButton()
table.register(CustomCell.self, forCellReuseIdentifier: "Cell")
table.delegate = self
table.dataSource = self
setupUI()
// Do any additional setup after loading the view.
}
func getListModels(noteModels: [CellModel]) {
listModels += noteModels
table.reloadData()
}
private func setupSearchText() {
searchText.translatesAutoresizingMaskIntoConstraints = false
searchText.font = UIFont.systemFont(ofSize: 14, weight: .bold)
searchText.textColor = .black
searchText.backgroundColor = .white
searchText.borderStyle = UITextField.BorderStyle.roundedRect
searchText.layer.borderWidth = 1
searchText.layer.borderColor = UIColor.black.cgColor
//date.textAlignment = .center
view.addSubview(searchText)
searchText.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
searchText.leadingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20
).isActive = true
searchText.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -70
).isActive = true
}
private func setupSearchButton() {
searchButton.translatesAutoresizingMaskIntoConstraints = false
// searchButton.font = UIFont.systemFont(ofSize: 14, weight: .bold)
// searchButton.textColor = .black
searchButton.backgroundColor = .white
// searchButton.borderStyle = UITextField.BorderStyle.roundedRect
searchButton.layer.borderWidth = 1
searchButton.setTitle("OK",
for: .normal)
// searchButton.titleLabel?.font = UIFont.systemFont(ofSize: 24)
searchButton.setTitleColor(.black, for: .normal)
searchButton.layer.borderColor = UIColor.black.cgColor
//date.textAlignment = .center
view.addSubview(searchButton)
searchButton.addTarget(self, action: #selector(didSearchButtonnTap(_:)), for: .touchUpInside)
searchButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
searchButton.leadingAnchor.constraint(
equalTo: searchText.safeAreaLayoutGuide.trailingAnchor, constant: 10
).isActive = true
searchButton.trailingAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20
).isActive = true
}
private func setupUI() {
table.translatesAutoresizingMaskIntoConstraints = false
table.separatorStyle = .none
view.addSubview(table)
table.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
table.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
table.topAnchor.constraint(equalTo: searchText.bottomAnchor,constant: 20).isActive = true
table.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
listModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? CustomCell else {
fatalError("не CustomCell")
}
cell.selectionStyle = .none
cell.model = listModels[indexPath.row]
cell.layer.cornerRadius = 14
cell.layer.shadowRadius = 14
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// нажатие
}
#objc private func didSearchButtonnTap(_ sender: Any) {
worker.fetch(question: searchText.text ?? "")
table.reloadData()
}
}
class Worker
import Foundation
class Worker {
let session: URLSession
var workerDelegate: WorkerDelegate?
func fetch(question: String) {
var listModels: [CellModel] = [CellModel(title: "123",name: "312",date: Date.now, answer_count: Int32(0)
)]
if let url = createURLComponents(question: question) {
url.asyncDownload { data, _, error in
guard let data = data else {
print("URLSession dataTask error:", error ?? "nil")
return
}
do {
let jsonObject = try JSONDecoder().decode(SearchModels.self, from: data)
_ = String(data: data, encoding: .utf8)
for object in jsonObject.items {
let timeInterval = TimeInterval(object.creationDate)
let newDate = Date(timeIntervalSince1970: timeInterval)
var model = CellModel(
title: object.title,
name: object.owner.displayName,
date: newDate,
answer_count: Int32(object.answerCount)
)
listModels.append(model)
}
DispatchQueue.main.async {
self.workerDelegate?.getListModels(noteModels: listModels)
}
} catch {
print(error.localizedDescription)
}
}
}
}
init(
session: URLSession = URLSession(configuration: .default)
) {
self.session = session
}
private func createURLComponents(question: String) -> URL? {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "api.stackexchange.com"
urlComponents.path = "/2.3/search"
urlComponents.queryItems = [
URLQueryItem(name: "order", value: "desc"),
URLQueryItem(name: "sort", value: "activity"),
URLQueryItem(name: "intitle", value: question),
URLQueryItem(name: "site", value: "stackoverflow")
]
return urlComponents.url!
}
private func createURLRequest(question: String) -> URLRequest {
var request = URLRequest(url: createURLComponents(question: question)!)
request.httpMethod = "GET"
return request
}
}
extension URL {
func asyncDownload(completion: #escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) {
URLSession.shared
.dataTask(with: self, completionHandler: completion)
.resume()
}
}

The background of the TableView at the bottom displays a blank cell

I have this UITableView that has xib files as cells and it all works as intended, however the last cell only when I scroll down doesn't have a background color. It only happens when I scroll down and then it pops back to normal position. I'm essentially wondering how I can change the color of that cell just to my background color. Thank you.
//
// BlogViewController.swift
// testAPI
//
// Created by Dilan Piscatello on 4/2/20.
// Copyright © 2020 Dilan Piscatello. All rights reserved.
//
import Foundation
import UIKit
import Firebase
class BlogViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
var x = 0
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0{
return 1
}else{
return posts.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
let cell = Bundle.main.loadNibNamed("QuestionTableViewCell", owner: self, options: nil)?.first as! QuestionTableViewCell
print("this is how many times it ran")
cell.setPost(question: self.question)
return cell
}
else{
let cell = tableView.dequeueReusableCell(withIdentifier: "postcell", for: indexPath) as! PostTableViewCell
cell.setPost(post: posts[indexPath.row])
return cell
}
}
var question = "sdfsdfs"
var tableView:UITableView!
var lastUploadPostID:String?
var discussion:UILabel! = UILabel()
var posts = [Post]()
var fetchingMore = false
var endReached = false
let leadingScreensForBatching:CGFloat = 3.0
var cellHeights: [IndexPath:CGFloat] = [:]
var refreshControl:UIRefreshControl!
var postRef:DatabaseReference{
return Database.database().reference().child("posts")
}
var oldPostQuery:DatabaseQuery{
var queryRef:DatabaseQuery
let lastPost = self.posts.last
if lastPost == nil{
queryRef = postRef.queryOrdered(byChild: "timestamp")
}else{
let lastTimestamp = lastPost!.createdAt.timeIntervalSince1970*1000
queryRef = postRef.queryOrdered(byChild: "timestamp").queryEnding(atValue: lastTimestamp)
}
return queryRef
}
var newPostQuery:DatabaseQuery{
var queryRef:DatabaseQuery
let firstPost = self.posts.first
if firstPost == nil{
queryRef = postRef.queryOrdered(byChild: "timestamp")
}else{
let firstTimestamp = firstPost!.createdAt.timeIntervalSince1970*1000
queryRef = postRef.queryOrdered(byChild: "timestamp").queryStarting(atValue: firstTimestamp)
}
return queryRef
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func hi(){
let db = Firestore.firestore()
db.collection("blog").document("question").getDocument { (document,error) in
if error != nil{
print("cant get data")
}
if document != nil && document!.exists{
if let documentdata = document?.data() {
self.question = documentdata["question"] as! String
self.tableView.reloadData()
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
discussion.text = "Discussion"
//longTitleLabel.font = ................
discussion.font = UIFont(name: "HelveticaNeue-Bold", size: 31)
discussion.translatesAutoresizingMaskIntoConstraints = false
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.addSubview(discussion)
//navigationBar.shadowImage = UIImage()
// navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
// navigationBar.isTranslucent = true
navigationBar.barTintColor = UIColor( red: 128/255, green: 117/255, blue: 255/255, alpha: 1)
discussion.leftAnchor.constraint(equalTo: navigationBar.leftAnchor, constant: 22).isActive = true
discussion.widthAnchor.constraint(equalToConstant: 300).isActive = true
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.separatorStyle = .none
view.addSubview(tableView)
hi()
print(question)
// Do any additional setup after loading the view.
let cellNib = UINib(nibName: "PostTableViewCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "postcell")
var layoutGuide:UILayoutGuide!
layoutGuide = view.safeAreaLayoutGuide
tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
tableView.reloadData()
refreshControl = UIRefreshControl()
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
beginBatchFetch()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// if let navigationController = navigationController as? ScrollingNavigationController {
// navigationController.followScrollView(tableView, delay: 0.0)
//}
listenForNewPosts()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopListeningForNewPosts()
}
#objc func handleRefresh(){
print()
newPostQuery.queryLimited(toFirst: 20).observeSingleEvent(of: .value, with: { (snapshot) in
var tempPosts = [Post]()
let firstPost = self.posts.first
for child in snapshot.children{
if let childSnapshot = child as? DataSnapshot,
let dict = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, dict),
childSnapshot.key != firstPost?.id{
tempPosts.insert(post, at: 0)
}
}
self.posts.insert(contentsOf: tempPosts, at: 0)
self.tableView.reloadData()
self.refreshControl.endRefreshing()
//let newIndexPaths = (1..<tempPosts.count).map { i in
// return IndexPath(row: i, section: 1)
//}
//self.tableView.insertRows(at: newIndexPaths, with: .top)
// self.refreshControl.endRefreshing()
// self.tableView.scrollToRow(at: IndexPath(row:0,section: 0), at: .top, //animated:true)
//self.listenForNewPosts()
//return completion(tempPosts)
//self.posts = tempPosts
//self.tableView.reloadData()
})
}
func fetchPosts(completion: #escaping(_ posts:[Post])->()){
oldPostQuery.queryLimited(toLast: 20).observeSingleEvent(of: .value, with: { (snapshot) in
var tempPosts = [Post]()
let lastPost = self.posts.last
for child in snapshot.children{
if let childSnapshot = child as? DataSnapshot,
let dict = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, dict),
childSnapshot.key != lastPost?.id{
tempPosts.insert(post, at: 0)
}
}
return completion(tempPosts)
//self.posts = tempPosts
//Zself.tableView.reloadData()
})
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
cell.selectionStyle = UITableViewCell.SelectionStyle.none
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 72.0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height * leadingScreensForBatching {
if !fetchingMore && !endReached {
beginBatchFetch()
}
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
func beginBatchFetch(){
fetchingMore = true
fetchPosts{ newPosts in
self.posts.append(contentsOf: newPosts)
self.endReached = newPosts.count == 0
self.fetchingMore = false
UIView.performWithoutAnimation {
self.tableView.reloadData()
self.listenForNewPosts()
}
}
//fetch the post
}
var postListenerHandle:UInt?
func listenForNewPosts(){
guard !fetchingMore else{ return}
//to avoid the listeners twice (duplicate)
stopListeningForNewPosts()
postListenerHandle = newPostQuery.observe(.childAdded) { (snapshot) in
if snapshot.key != self.posts.first?.id {
if let data = snapshot.value as? [String:Any],
//let post = Post.parse(snapshot.key,data)
let _ = Post.parse(snapshot.key,data){
self.stopListeningForNewPosts()
if snapshot.key == self.lastUploadPostID{
self.handleRefresh()
self.lastUploadPostID = nil
}else{
}
}
}
}
}
func stopListeningForNewPosts(){
if let handle = postListenerHandle{
newPostQuery.removeObserver(withHandle: handle)
postListenerHandle = nil
}
}
// func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
//if let navigationController = navigationController as? //ScrollingNavigationController {
// navigationController.showNavbar(animated: true, scrollToTop: true)
//}
// return true
//}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let newPostNavBar = segue.destination as? UINavigationController,
let newPostVC = newPostNavBar.viewControllers[0] as? WritePostViewController{
newPostVC.delegate = self
}
}
}
extension BlogViewController: NewPostVCDelegate{
func didUploadPost(withID id: String) {
print("what up this is the id \(id)")
self.lastUploadPostID = id
}
}
In the storyboard add inside the tableView as last element a transparent view with height equal to 1

In keeping with the recent questions on closures used to pass data between VCs, how would I do the same when using a containerVC within the rootVC?

I saw a recent bountied question (can find the link if you wish to see it) about using closures to pass data between VCs where one VC was embedded in a navigation controller. While the use of a closure there was fairly easy since there was a direct point of contact between the two VCs (in the form a segue), I have been wondering how the same would work if this was not the case.
As an example, consider the following set up (similar to the OG question that inspired this post):
RootVC, which has a counter UILabel
A subContainer VC which takes up the lower half of RootVC, which has a button, pressing which should increment the UILabel on RootVC by one.
I have prepared the code as follows (with some code taken from the OG question):
RootVC:
class RootVC: UIViewController {
var tappedCount: Int = 0
let pagingContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var label: UILabel = {
let label = UILabel()
label.text = "\(tappedCount)"
label.textAlignment = .center
label.font = UIFont(name: "Copperplate", size: 90)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(label)
view.addSubview(pagingContainer)
pagingContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
pagingContainer.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1).isActive = true
pagingContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
pagingContainer.heightAnchor.constraint(equalToConstant: 500).isActive = true
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
addChild(pageController)
pageController.didMove(toParent: self)
pageController.view.translatesAutoresizingMaskIntoConstraints = false
pagingContainer.addSubview(pageController.view)
pageController.view.heightAnchor.constraint(equalTo: pagingContainer.heightAnchor, multiplier: 1).isActive = true
pageController.view.widthAnchor.constraint(equalTo: pagingContainer.widthAnchor, multiplier: 1).isActive = true
pageController.view.topAnchor.constraint(equalTo: pagingContainer.topAnchor).isActive = true
pageController.view.bottomAnchor.constraint(equalTo: pagingContainer.bottomAnchor).isActive = true
pageController.view.leadingAnchor.constraint(equalTo: pagingContainer.leadingAnchor).isActive = true
pageController.view.trailingAnchor.constraint(equalTo: pagingContainer.trailingAnchor).isActive = true
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: pagingContainer.topAnchor).isActive = true
}
}
SubContainerVC:
class SubContainerVC: UIViewController {
var callback : (() -> Void)?
let button: UIButton = {
let button = UIButton()
button.setTitle("Button!", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.backgroundColor = .green
return button
}()
#objc func buttonPressed(_ sender: UIButton) {
print("Hello")
//Pressing this button should increment the label on RootVC by one.
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
view.addSubview(button)
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
And the PageViewController swift file:
class PageViewController: UIPageViewController {
lazy var subViewControllers:[UIViewController] = {
return [SubContainerVC()]
}()
init(transitionStyle style:
UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [String : Any]? = nil) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
setViewControllerFromIndex(index: 0)
}
func setViewControllerFromIndex(index:Int) {
self.setViewControllers([subViewControllers[index]], direction: UIPageViewController.NavigationDirection.forward, animated: true, completion: nil)
}
}
extension PageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return subViewControllers.count
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.firstIndex(of: viewController) ?? 0
if currentIndex <= 0 {
return nil
}
return subViewControllers[currentIndex-1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.firstIndex(of: viewController) ?? 0
if currentIndex >= subViewControllers.count-1 {
return nil
}
return subViewControllers[currentIndex+1]
}
}
You can inject the closure downstream to SubContainerVC, this will result in the closure execution coming up upstream.
Something along the lines (kept only the relevant VC code):
class SubContainerVC {
var buttonCallback: () -> Void = { }
#objc func buttonPressed(_ sender: UIButton) {
print("Hello")
buttonCallback()
}
}
class PageViewController: UIViewController {
// Note that you don't need the extra closure call for lazy vars
lazy var subViewControllers = [SubContainerVC()] {
didSet {
// just in case the controllers might change later on
subViewControllers.forEach { $0.buttonCallback = buttonCallback }
}
}
var buttonCallback: () -> Void = { } {
didSet {
subViewControllers.forEach { $0.buttonCallback = buttonCallback }
}
}
}
class RootVC: UIViewController {
var tappedCount: Int = 0 {
didSet {
label.text = "\(tappedCount)"
}
}
override func viewDidLoad() {
super.viewDidLoad()
let pageController = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
// this will trigger the `didSet` from PageViewController, resulting
// in the callback being propagated downstream
pageController.buttonCallback = { self.tappedCount += 1 }
}
}

UITextField is partially hidden by Keyboard when opened

I am attempting to create a collection of UITextField elements. I'd like the next button on the keyboard to skip to the next field and if that field is hidden from view by the keyboard, scroll it into view.
This is my attempt. It works apart from 1 aspect.
When dismissing the keyboard and then selecting another (or the same) field, the text input is partially hidden by the keyboard (see attached gif).
The meat and potatoes is within the ViewController extension.
class ViewController: UIViewController {
var activeField: UITextField?
var lastOffset: CGPoint!
var keyboardHeight: CGFloat!
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let scrollViewContainer: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 10
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(scrollView)
scrollView.addSubview(scrollViewContainer)
let totalFieldCount = 25
for i in 1...totalFieldCount {
let textField = createTextField(self, placeholder: "Field #\(i)", type: .default)
textField.returnKeyType = i < totalFieldCount ? .next : .done
textField.tag = i
scrollViewContainer.addArrangedSubview(textField)
}
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollViewContainer.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
scrollViewContainer.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
scrollViewContainer.topAnchor.constraint(equalTo: scrollView.topAnchor),
scrollViewContainer.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
scrollViewContainer.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
scrollView.keyboardDismissMode = .interactive
}
func createTextField(_ delegate: UITextFieldDelegate?, placeholder: String, type: UIKeyboardType, isSecureEntry: Bool = false) -> UITextField {
let tf = UITextField(frame: .zero)
tf.placeholder = placeholder
tf.backgroundColor = .init(white: 0, alpha: 0.03)
tf.borderStyle = .roundedRect
tf.font = .systemFont(ofSize: 14)
tf.keyboardType = type
tf.autocapitalizationType = .none
tf.autocorrectionType = .no
tf.isSecureTextEntry = isSecureEntry
tf.heightAnchor.constraint(equalToConstant: 40).isActive = true
if let delegate = delegate {
tf.delegate = delegate
}
return tf
}
}
extension ViewController: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
activeField = textField
lastOffset = self.scrollView.contentOffset
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let nextTag = textField.tag + 1
if let nextResponder = textField.superview?.viewWithTag(nextTag) {
nextResponder.becomeFirstResponder()
} else {
activeField?.resignFirstResponder()
activeField = nil
}
return true
}
}
extension ViewController {
#objc func keyboardWillShow(notification: NSNotification) {
guard keyboardHeight == nil else { return }
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
keyboardHeight = keyboardSize.height
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.contentInset.bottom = self.keyboardHeight
})
guard let activeField = activeField else { return }
let distanceToBottom = self.scrollView.frame.size.height - (activeField.frame.origin.y) - (activeField.frame.size.height)
let collapseSpace = keyboardHeight - distanceToBottom
guard collapseSpace > 0 else { return }
UIView.animate(withDuration: 0.3, animations: {
self.scrollView.contentOffset = CGPoint(x: self.lastOffset.x, y: collapseSpace + 10)
})
}
}
#objc func keyboardWillHide(notification: NSNotification) {
UIView.animate(withDuration: 0.3) {
self.scrollView.contentOffset = self.lastOffset
self.scrollView.contentInset.bottom = 0
}
keyboardHeight = nil
}
}
Replace keyboardFrameBeginUserInfoKey with keyboardFrameEndUserInfoKey