So I have
let verticalViewOne = UIView()
I want to add the following button to this view
/* Station */
DRN1Button.tintColor = .systemPink
DRN1Button.frame.size = CGSize(width: 200.0, height: 200.0)
DRN1Button.setImage(UIImage(named: "DRN1.jpg"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//playButton.imageView?.contentMode = .scaleAspectFit
DRN1Button.contentVerticalAlignment = .fill
DRN1Button.contentHorizontalAlignment = .fill
DRN1Button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
DRN1Button.addTarget(self, action: #selector(playdrn1), for: .touchUpInside)
DRN1Button.translatesAutoresizingMaskIntoConstraints = false
// self.view.addSubview(playButton)
verticalStackView.addSubview(DRN1Button)
My question is how do I go about this?
as this code
verticalStackView.addSubview(DRN1Button)
brings an error that crashes the app.
NOTE: the button is override func viewDidLoad() { while the verticalStackView is set in
func setupHierachy(){
view.addSubview(containerStackView)
view.addSubview(verticalStackView)
verticalStackView.addArrangedSubview(verticalViewOne)
//verticalStackView.addArrangedSubview(DRN1Button)
containerStackView.addArrangedSubview(verticalStackView)
}
Error am getting is
Thread 1: Exception: "Unable to activate constraint with anchors <NSLayoutYAxisAnchor:0x281f52640 \"UIButton:0x10540ba90.centerY\"> and <NSLayoutYAxisAnchor:0x281f52680 \"UIView:0x10530ea40.centerY\"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal."
full code.
//
// ViewController.swift
// 1life.radio
//
// Created by Russell Harrower on 19/3/20.
// Copyright © 2020 Russell Harrower. All rights reserved.
//
import UIKit
import AVKit
import Network
import Kingfisher
struct Nowplayng: Decodable{
let data: [Data]
}
struct Data: Decodable{
let track: [Trackinfo]
}
struct Trackinfo: Decodable {
let title: String
let artist: String
let imageurl: String
}
class ViewController: UIViewController {
let uuid = UIDevice.current.identifierForVendor?.uuidString
let playButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100))
let DRN1Button = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 48))
var play = 0;
var npinfo = "https://api.drn1.com.au/station/1lifeRadio/playing"
var currentstation = "";
var timer = Timer()
/* V VIEWS */
let containerStackView = UIStackView()
let verticalStackView = UIStackView()
let verticalViewOne = UIView()
let verticalViewTwo = UIView()
let verticalViewThree = UIView()
/* END V VIEWS */
var isPlaying = false {
didSet {
self.playButton.setImage(UIImage(systemName: self.isPlaying ? "play.circle" : "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//self.isPlaying ? MusicPlayer.shared.startBackgroundMusic() : MusicPlayer.shared.stopBackgroundMusic()
self.isPlaying ? MusicPlayer.shared.stopBackgroundMusic() : MusicPlayer.shared.startBackgroundMusic(url: currentstation)
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupHierachy()
setlayout()
currentstation = "http://stream.radiomedia.com.au:8015/stream?uuid=\(self.uuid ?? "")"
// Do any additional setup after loading the view.
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8015/stream?uuid=\(self.uuid ?? "")")
playButton.tintColor = .systemPink
playButton.frame.size = CGSize(width: 200.0, height: 200.0)
playButton.setImage(UIImage(systemName: "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
//playButton.imageView?.contentMode = .scaleAspectFit
playButton.contentVerticalAlignment = .fill
playButton.contentHorizontalAlignment = .fill
playButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
playButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
playButton.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(playButton)
//verticalViewOne.addSubview(DRN1Button)
NSLayoutConstraint.activate([
playButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
playButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
playButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -208),
//playButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
//playButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
playButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
playButton.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
/* DRN1Button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
DRN1Button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
DRN1Button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
//playButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 12),
//playButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
DRN1Button.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2),
DRN1Button.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.2)*/
])
self.artist.textAlignment = .center
self.song.textAlignment = .center
nowplaying()
scheduledTimerWithTimeInterval()
}
func setupViews(){
verticalStackView.axis = .vertical
verticalStackView.translatesAutoresizingMaskIntoConstraints = true
verticalStackView.distribution = .fillEqually
verticalViewOne.backgroundColor = .red
containerStackView.backgroundColor = .green
containerStackView.axis = .vertical
containerStackView.spacing = 15
containerStackView.distribution = .fillEqually
}
func setupHierachy(){
view.addSubview(containerStackView)
view.addSubview(verticalStackView)
verticalStackView.addArrangedSubview(verticalViewOne)
//verticalStackView.addArrangedSubview(DRN1Button)
containerStackView.addArrangedSubview(verticalStackView)
}
func setlayout(){
containerStackView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
containerStackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2).isActive = true
containerStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
containerStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
}
#objc func appDidLaunch() {
#if targetEnvironment(simulator)
/*
if you don’t set up remote controls, the AVPlayer from
AVPlayerController will immediately create default remote controls
otherwise your simulator will not show the controls.
*/
self.isPlaying = false
self.playButton.setImage(UIImage(systemName: "pause.circle"), for: UIControl.State(rawValue: UIControl.State.RawValue(play)))
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8015/stream?uuid=\(uuid ?? "")")
#else
// MusicPlayer.shared.setupRemoteTransportControls()
// more custom controls set up
#endif
}
#objc func playdrn1(sender:UIButton!){
currentstation = "http://stream.radiomedia.com.au:8003/stream?uuid=\(uuid ?? "")"
MusicPlayer.shared.startBackgroundMusic(url: "http://stream.radiomedia.com.au:8003/stream?uuid=\(uuid ?? "")")
npinfo = "https://api.drn1.com.au/station/DRN1/playing"
}
#objc func buttonAction(sender: UIButton!) {
print(self.isPlaying)
self.isPlaying = !self.isPlaying
}
#IBOutlet var imageurl: UIImageView!
#IBOutlet var artist: UILabel!
#IBOutlet var song: UILabel!
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 1 seconds
timer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(self.nowplaying), userInfo: nil, repeats: true)
}
#objc func nowplaying(){
let jsonURLString = npinfo
guard let feedurl = URL(string: jsonURLString) else { return }
URLSession.shared.dataTask(with: feedurl) { (data,response,err)
in
guard let data = data else { return }
do{
let nowplaying = try JSONDecoder().decode(Nowplayng.self, from: data)
//print(nowplaying.data.first?.track.first?.artist as Any)
nowplaying.data.forEach {_ in
DispatchQueue.main.async {
self.artist.text = nowplaying.data.first?.track.first?.artist
self.song.text = nowplaying.data.first?.track.first?.title
}
if var strUrl = nowplaying.data.first?.track.first?.imageurl {
strUrl = strUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
DispatchQueue.main.sync {
let processor = DownsamplingImageProcessor(size: self.imageurl.bounds.size)
|> RoundCornerImageProcessor(cornerRadius: 20)
self.imageurl.kf.indicatorType = .activity
self.imageurl.kf.setImage(
with: URL(string: strUrl),
placeholder: UIImage(named: "placeholderImage"),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(1)),
.cacheSerializer(FormatIndicatedCacheSerializer.png),
.cacheOriginalImage
])
{
result in
switch result {
case .success(let value):
print("Task done for: \(value.source.url?.absoluteString ?? "")")
case .failure(let error):
print("Job failed: \(error.localizedDescription)")
}
}
}
// self.imageurl.kf.setImage(with: URL(string: strUrl), placeholder: nil)
//MusicPlayer.shared.nowplaying(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
// MusicPlayer.shared.getArtBoard(artist: $0.track.artist, song: $0.track.title, cover:strUrl)
}
}
}catch let jsonErr{
print("error json ", jsonErr)
}
}.resume()
}
}
First off, the line:
verticalStackView.addSubview(DRN1Button)
crashes because you are attempting to add a subview to stackView, you're supposed to do addArrangedSubview.
For the crash:
Thread 1: Exception: "Unable to activate constraint with anchors
<NSLayoutYAxisAnchor:0x281f52640 "UIButton:0x10540ba90.centerY"> and
<NSLayoutYAxisAnchor:0x281f52680 "UIView:0x10530ea40.centerY">
because they have no common ancestor. Does the constraint or its
anchors reference items in different view hierarchies? That's
illegal."
You have it already.
DRN1Button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
Your DRN1Button has no common ancestor with your self.view. This can mean that you haven't added your DRN1Button to a view, or most probably the line above, can't be done. While your button is in a stackview but you are setting its centerYAnchor equal to your self.view.
Related
I guess that it is a low brainer I'm struggling with, but unfortunately all my searches in this forum and other sources didn't give me a glue yet.
I'm creating a shopping list app for iOS. In the Viewcontroller for the entry of the shoppinglist positions I'm showing only the relevant entry fields depending on the kind of goods to be put on the shopping list.
Hence I have set up a tableView with different prototype cells and some of them contain UITextFields to handle this dynamic setup.
I have defined a toolbar for the keyboard containing one button at the right to hide the keyboard (which works) and two buttons ("next" & "back") on the left to jump to the next respectively previous input field, which should then become first responder, cursor set in this field and showing the keyboard.
Unfortunately this handing over of the firstResponder isn't working and the cursor is not set to the next/previous input field and sometimes even the keyboard disappears.
Jumping back doesn't work at all and the keyboard disappears always when the next active field is part of a different prototype cell (e.g. moving forward from the field for "brand" to the field for "quantity".
Has anyone a solution for it?
For the handling I have defined two notifications:
let keyBoardBarBackNotification = Notification.Name("keyBoardBarBackNotification")
let keyBoardBarNextNotification = Notification.Name("keyBoardBarNextNotification")
And the definition of the toolbar is done in the extension of UIViewController:
func setupKeyboardBar() -> UIToolbar {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 50))
let leftButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(leftButtonTapped))
leftButton.tintColor = UIColor.systemBlue
let nextButton = UIBarButtonItem(image: UIImage(systemName: "chevron.right"), style: .plain, target: self, action: #selector(nextButtonTapped))
nextButton.tintColor = UIColor.systemBlue
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let fixSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .plain, target: self, action: #selector(doneButtonTapped))
doneButton.tintColor = UIColor.darkGray
toolbar.setItems([fixSpace, leftButton, fixSpace, nextButton, flexSpace, doneButton], animated: true)
toolbar.sizeToFit()
return toolbar
}
#objc func leftButtonTapped() {
view.endEditing(true)
NotificationCenter.default.post(Notification(name: keyBoardBarBackNotification))
}
#objc func nextButtonTapped() {
view.endEditing(true)
NotificationCenter.default.post(Notification(name: keyBoardBarNextNotification))
}
#objc func doneButtonTapped() {
view.endEditing(true)
}
}
In the viewController I have setup routines for the keyboard handling and a routine "switchActiveField" to determine the next actual field that should become the firstResponder:
class AddPositionVC: UIViewController {
#IBOutlet weak var menue: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.menue.delegate = self
self.menue.dataSource = self
self.menue.separatorStyle = .none
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleBackButtonPressed), name: keyBoardBarBackNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleNextButtonPressed), name: keyBoardBarNextNotification, object: nil)
}
enum TableCellType: String {
case product = "Product:"
case brand = "Brand:"
case quantity = "Quantity:"
case price = "Price:"
case shop = "Shop:"
// ...
}
var actualField = TableCellType.product // field that becomes firstResponder
// Arrray, defining the fields to be diplayed
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop
]
// Array with IndexPath of displayed fields
var tableViewIndex = Dictionary<TableCellType, IndexPath>()
#objc func handleKeyboardDidShow(notification: NSNotification) {
guard let endframeKeyboard = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect else { return }
let insets = UIEdgeInsets( top: 0, left: 0, bottom: endframeKeyboard.size.height - 60, right: 0 )
self.menue.contentInset = insets
self.menue.scrollIndicatorInsets = insets
self.scrollToMenuezeile(self.actualField)
self.view.layoutIfNeeded()
}
#objc func handleKeyboardWillHide() {
self.menue.contentInset = .zero
self.view.layoutIfNeeded()
}
#objc func handleBackButtonPressed() {
switchActiveField(self.actualField, back: true)
}
#objc func handleNextButtonPressed() {
switchActiveField(self.actualField, back: false)
}
// Definition, which field should become next firstResponder
func switchActiveField(_ art: TableCellType, back bck: Bool) {
switch art {
case .brand:
self.actualField = bck ? .product : .quantity
case .quantity:
self.actualField = bck ? .brand : .shop
case .price:
self.actualField = bck ? .quantity : .shop
case .product:
self.actualField = bck ? .shop : .brand
case .shop:
self.actualField = bck ? .price : .product
// ....
}
if let index = self.tableViewIndex[self.actualField] {
self.menue.reloadRows(at: [index], with: .automatic)
}
}
}
And the extension for the tableView is:
extension AddPositionVC: UITableViewDelegate, UITableViewDataSource {
func scrollToMenuezeile(_ art: TableCellType) {
if let index = self.tableViewIndex[art] {
self.menue.scrollToRow(at: index, at: .bottom, animated: false)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menueList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableCellType = self.menueList[indexPath.row]
self.tableViewIndex[tableCellType] = indexPath
switch tableCellType {
case .product, .brand, .shop:
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelTextFieldCell", for: indexPath) as! LabelTextFieldCell
cell.item.text = tableCellType.rawValue
cell.itemInput.inputAccessoryView = self.setupKeyboardBar()
cell.itemInput.text = "" // respective Input
if self.actualField == tableCellType {
cell.itemInput.becomeFirstResponder()
}
return cell
case .quantity, .price:
let cell = tableView.dequeueReusableCell(withIdentifier: "QuantityPriceCell", for: indexPath) as! QuantityPriceCell
cell.quantity.inputAccessoryView = self.setupKeyboardBar()
cell.quantity.text = "" // respective Input
cell.price.inputAccessoryView = self.setupKeyboardBar()
cell.price.text = "" // respective Input
if self.actualField == .price {
cell.price.becomeFirstResponder()
} else if self.actualField == .quantity {
cell.quantity.becomeFirstResponder()
}
return cell
}
}
}
//*********************************************
// MARK: - tableViewCells
//*********************************************
class LabelTextFieldCell: UITableViewCell, UITextFieldDelegate {
override func awakeFromNib() {
super.awakeFromNib()
itemInput.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
self.itemInput.resignFirstResponder()
}
#IBOutlet weak var item: UILabel!
#IBOutlet weak var itemInput: UITextField!
}
class QuantityPriceCell: UITableViewCell, UITextFieldDelegate {
override func awakeFromNib() {
super.awakeFromNib()
self.quantity.delegate = self
self.price.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
textField.resignFirstResponder()
}
#IBOutlet weak var quantity: UITextField!
#IBOutlet weak var price: UITextField!
}
Thanks for your support.
There are various ways to approach this... In fact, it's easy to find open-source 3rd-party libraries with lots of features -- just search (Google or wherever) for swift ios form builder.
But, if you'd like to work on it on your own, the basic idea is:
add your text fields to an array
add a class-level var/property such as var activeField: UITextField?
for each field, on textFieldDidBeginEditing:
self.activeField = textField
when the user taps the "Next" button:
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == self.textFields.count - 1 {
// "wrap around" to first field
textFields.first?.becomeFirstResponder()
} else {
// "move to" next field
textFields[idx + 1].becomeFirstResponder()
}
If all your fields are "on-screen" it's pretty straight-forward.
If they won't fit vertically (particularly when the keyboard is showing), if they're all in a scroll view, again, pretty straight-forward.
It gets complicated when putting them in cells in a tableView, for several reasons:
cells are not necessarily generated in order, so you have to write a bunch more code to put move from field-to-field in the correct order
if you have more cells than will fit on-screen, the "next field" may not exist! For example, suppose you have 8 rows... only 5 rows fit... you're editing the field in the last row and tap the Next button. You want to move to the field in Row 0, but Row 0 won't exist until you scroll back up to the top.
To add repeating similar-but-varying "rows," we don't need to use a table view.
For example, if we have a UIStackView with .axis = .vertical:
for i in 1...10 {
let label = UILabel()
label.text = "Row \(i)"
stackView.addArrangedSubview(label)
}
We've now added 10 single-label "cells."
So, for your task, instead of using a table view with your LabelTextFieldCell, we can write this function:
func buildLabelTextFieldView(labelText str: String) -> UIView {
let aView = UIView()
let label: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let field: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
label.text = str
self.textFields.append(field)
aView.addSubview(label)
aView.addSubview(field)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
])
return aView
}
and a similar (but slightly more complex):
func buildQuantityPriceView() -> UIView {
let aView = UIView()
...
return aView
}
then use it similarly to cellForRowAt:
for i in 0..<menueList.count {
let tableCellType = menueList[i]
var rowView: UIView!
switch tableCellType {
case .product, .brand, .shop:
rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)
case .quantity, .price:
rowView = buildQuantityPriceView()
}
stackView.addArrangedSubview(rowView)
}
If we add that stackView to a scrollView, we have a scrollable "Form."
Here's a complete example you can try out (no #IBOutlet or #IBAction connections ... just set a blank view controller's class to FormVC):
class FormVC: UIViewController, UITextFieldDelegate {
var textFields: [UITextField] = []
let scrollView = UIScrollView()
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop]
lazy var kbToolBar: UIToolbar = {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 50))
let leftButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(leftButtonTapped))
leftButton.tintColor = UIColor.systemBlue
let nextButton = UIBarButtonItem(image: UIImage(systemName: "chevron.right"), style: .plain, target: self, action: #selector(nextButtonTapped))
nextButton.tintColor = UIColor.systemBlue
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let fixSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .plain, target: self, action: #selector(doneButtonTapped))
doneButton.tintColor = UIColor.darkGray
toolbar.setItems([fixSpace, leftButton, fixSpace, nextButton, flexSpace, doneButton], animated: true)
toolbar.sizeToFit()
return toolbar
}()
var activeField: UITextField?
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 32
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -16.0),
stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
for i in 0..<menueList.count {
let tableCellType = menueList[i]
var rowView: UIView!
switch tableCellType {
case .product, .brand, .shop:
rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)
case .quantity, .price:
rowView = buildQuantityPriceView()
}
stackView.addArrangedSubview(rowView)
}
// we've added all the labels and fields
// and our textFields array contains all the fields in order
// we want all the "first/left" labels to be equal widths
guard let firstLabel = stackView.arrangedSubviews.first?.subviews.first as? UILabel
else {
fatalError("We did something wrong in our setup!")
}
stackView.arrangedSubviews.forEach { v in
// skip the first one
if v != stackView.arrangedSubviews.first {
if let thisLabel = v.subviews.first as? UILabel {
thisLabel.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
}
}
// set inputAccessoryView and delegate on all the text fields
textFields.forEach { v in
v.inputAccessoryView = kbToolBar
v.delegate = self
}
// prevent keyboard from hiding scroll view elements
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
// during dev, use "if true" and set some colors so we can see view framing
if false {
view.backgroundColor = .systemYellow
scrollView.backgroundColor = .yellow
stackView.layer.borderColor = UIColor.red.cgColor
stackView.layer.borderWidth = 1
stackView.arrangedSubviews.forEach { v in
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
}
}
}
#objc func leftButtonTapped() {
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == 0 {
textFields.last?.becomeFirstResponder()
} else {
textFields[idx - 1].becomeFirstResponder()
}
}
#objc func nextButtonTapped() {
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == self.textFields.count - 1 {
textFields.first?.becomeFirstResponder()
} else {
textFields[idx + 1].becomeFirstResponder()
}
}
#objc func doneButtonTapped() {
view.endEditing(true)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.activeField = textField
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.activeField = nil
}
#objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
self.scrollView.contentInset = .zero
} else {
self.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset
}
}
We'll put our "Row View" builder funcs in extensions, just to keep the code separated and a bit more readable:
extension FormVC {
func buildLabelTextFieldView(labelText str: String) -> UIView {
let aView = UIView()
let label: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let field: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
label.text = str
self.textFields.append(field)
aView.addSubview(label)
aView.addSubview(field)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
])
return aView
}
}
extension FormVC {
func buildQuantityPriceView() -> UIView {
let aView = UIView()
let labelA: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let fieldA: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
labelA.text = "Quantity:"
self.textFields.append(fieldA)
let labelB: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let fieldB: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
labelB.text = "Price:"
self.textFields.append(fieldB)
aView.addSubview(labelA)
aView.addSubview(fieldA)
aView.addSubview(labelB)
aView.addSubview(fieldB)
NSLayoutConstraint.activate([
labelA.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
labelA.firstBaselineAnchor.constraint(equalTo: fieldA.firstBaselineAnchor),
fieldA.leadingAnchor.constraint(equalTo: labelA.trailingAnchor, constant: 8.0),
fieldA.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
fieldA.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
labelB.leadingAnchor.constraint(equalTo: fieldA.trailingAnchor, constant: 8.0),
labelB.firstBaselineAnchor.constraint(equalTo: fieldB.firstBaselineAnchor),
fieldB.leadingAnchor.constraint(equalTo: labelB.trailingAnchor, constant: 8.0),
fieldB.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
fieldB.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
fieldB.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
// we want both fields to be equal widths
fieldB.widthAnchor.constraint(equalTo: fieldA.widthAnchor),
])
return aView
}
}
When running, it looks like this:
If you add some more "rows" - or, easier, increase the stack view spacing, such as stackView.spacing = 100 - you'll see how it continues to work with the scrollView when the keyboard is showing.
Of course, you mention in your comments: "...more entry fields (e.g. date with a Datepicker, etc.)", so you'd need to write new "row builder" funcs and add some logic to Next tap going to/from a Picker instead of a textField.
But, you may find this a helpful starting point.
When its clicked on some cell, DetailViewController will be opened. Problem is that context from DetailViewController is shown while transitioning between controllers.
Picture below presents problem:
image
DetailViewController is called in function:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let singleMovie = movieList[indexPath.row]
getSingleMovie(movieId: singleMovie.id, completion: {})
getDirector(movieId: singleMovie.id, completion: { [weak self] in
guard let self = self else { return }
var directorName = ""
self.creditResponse?.crew.forEach({ singleCredit in
if singleCredit.knownForDepartment == .directing {
directorName = singleCredit.name
}
})
guard let safeMovie = self.detailMovie else {return}
let detailVc = DetailViewController(movie: safeMovie, groups: self.checkGroups(groups: singleMovie.genreIds), director: directorName, movieIndex: indexPath.row)
self.navigationController?.pushViewController(detailVc, animated: true)
})
}
DetailViewController:
import UIKit
class DetailViewController: UIViewController {
let movie: Details?
let groupsValue: String?
let directorValue: String?
let movieIndex: Int?
var checkButton: Bool = false
var favoriteButton: Bool = false
let backButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "Icon"), for: .normal)
button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside)
return button
}()
var movieTitleLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Quicksand-Bold", size: 40)
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
var groupsLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Quicksand-Regular", size: 20)
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var directorLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Quicksand-Bold", size: 20)
label.textColor = .white
label.text = "Director: "
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var directorNameLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Quicksand-Regular", size: 20)
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var descriptionLabel: UILabel = {
let label = UILabel()
label.font = UIFont(name: "Quicksand-Regular", size: 20)
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
var movieImageView: UIImageView = {
let iv = UIImageView()
iv.layer.cornerRadius = 15
iv.layer.masksToBounds = true
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let buttonChecked: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = UIColor(red: 0.475, green: 0.729, blue: 0.757, alpha: 1)
button.imageView?.contentMode = .scaleAspectFit
button.imageEdgeInsets = UIEdgeInsets(top: 35, left: 35, bottom: 35, right: 35)
button.tintColor = UIColor(red: 0.475, green: 0.729, blue: 0.757, alpha: 1)
button.addTarget(self, action: #selector(checkButtonTapped), for: .touchUpInside)
return button
}()
let buttonFavorite: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = UIColor(red: 0.475, green: 0.729, blue: 0.757, alpha: 1)
button.imageView?.contentMode = .scaleAspectFit
button.imageEdgeInsets = UIEdgeInsets(top: 35, left: 35, bottom: 35, right: 35)
button.tintColor = UIColor(red: 0.475, green: 0.729, blue: 0.757, alpha: 1)
button.addTarget(self, action: #selector(favoriteButtonTapped), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationItem.hidesBackButton = true
setupUI()
}
init(movie:Details, groups: String, director: String, movieIndex: Int) {
self.movie = movie
self.groupsValue = groups
self.directorValue = director
self.movieIndex = movieIndex
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension DetailViewController{
#objc func backButtonTapped() {
navigationController?.popToRootViewController(animated: true)
}
}
extension DetailViewController {
func setupUI(){
view.addSubview(movieTitleLabel)
view.addSubview(groupsLabel)
view.addSubview(directorLabel)
view.addSubview(descriptionLabel)
view.addSubview(directorNameLabel)
view.addSubview(movieImageView)
view.addSubview(backButton)
view.addSubview(buttonChecked)
view.addSubview(buttonFavorite)
setupValues()
setupConstraints()
setupButtons()
}
}
extension DetailViewController {
func setupConstraints(){
let bottomImageConstraint = movieImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
bottomImageConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
movieImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
movieImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
movieImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomImageConstraint,
movieImageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.3),
movieTitleLabel.topAnchor.constraint(equalTo: movieImageView.bottomAnchor, constant: 10),
movieTitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
movieTitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
groupsLabel.topAnchor.constraint(equalTo: movieTitleLabel.bottomAnchor, constant: 10),
groupsLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
directorLabel.topAnchor.constraint(equalTo: groupsLabel.bottomAnchor, constant: 10),
directorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
directorLabel.topAnchor.constraint(equalTo: groupsLabel.bottomAnchor, constant: 10),
directorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
directorNameLabel.topAnchor.constraint(equalTo: groupsLabel.bottomAnchor, constant: 10),
directorNameLabel.leadingAnchor.constraint(equalTo: directorLabel.trailingAnchor, constant: 10),
descriptionLabel.topAnchor.constraint(equalTo: directorLabel.bottomAnchor, constant: 10),
descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
backButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
backButton.heightAnchor.constraint(equalToConstant: 40),
backButton.widthAnchor.constraint(equalToConstant: 40),
buttonFavorite.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
buttonFavorite.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
buttonChecked.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
buttonChecked.trailingAnchor.constraint(equalTo: buttonFavorite.trailingAnchor, constant: -50),
])
}
func setupValues(){
movieTitleLabel.text = movie?.title
guard let safeUrl = movie?.posterPath else {return}
movieImageView.downloadImage(from: safeUrl)
descriptionLabel.text = movie?.overview
groupsLabel.text = groupsValue
directorNameLabel.text = directorValue
}
func setupButtons(){
let checkValue = UserDefaults.standard.bool(forKey: "checkButton\(movieIndex ?? 0)")
let favoriteValue = UserDefaults.standard.bool(forKey: "favoriteButton\(movieIndex ?? 0)")
if checkValue == true {
buttonChecked.setImage(UIImage(systemName: "checkmark.seal.fill"), for: .normal)
}
else {
buttonChecked.setImage(UIImage(systemName: "checkmark.seal"), for: .normal)
}
if favoriteValue == true {
buttonFavorite.setImage(UIImage(systemName: "star.fill"), for: .normal)
}
else {
buttonFavorite.setImage(UIImage(systemName: "star"), for: .normal)
}
}
}
extension DetailViewController{
#objc func checkButtonTapped() {
checkButton = !checkButton
if checkButton == true {
DispatchQueue.main.async {
self.buttonChecked.setImage(UIImage(systemName: "checkmark.seal.fill"), for: .normal)
}
}
else {
DispatchQueue.main.async {
self.buttonChecked.setImage(UIImage(systemName: "checkmark.seal"), for: .normal)
}
}
UserDefaults.standard.set(checkButton, forKey: "checkButton\(movieIndex ?? 0)")
}
#objc func favoriteButtonTapped() {
favoriteButton = !favoriteButton
if favoriteButton == true {
DispatchQueue.main.async {
self.buttonFavorite.setImage(UIImage(systemName: "star.fill"), for: .normal)
}
}
else {
DispatchQueue.main.async {
self.buttonFavorite.setImage(UIImage(systemName: "star"), for: .normal)
}
}
UserDefaults.standard.set(favoriteButton, forKey: "favoriteButton\(movieIndex ?? 0)")
}
}
How to setup animations so it transits normally?
Transition GIF
It looks like your DetailViewController.view has no backgroundColor set (OR it is .clear).
Please assign a backgroundColor to your view like this.
override func viewDidLoad() {
super.viewDidLoad()
// For testing, you may want to set this to something else like `.red` / `.yellow`
// This will help you in identifying where the issue is
self.view.backgroundColor = .black
}
I am trying to make an image "move down" every second while also showing a time counter. I have a scheduledTimer where I change the image's Y center and update the timer. The timer label updates but the image does not move. Strangely I can get the image to move down if I comment the line where I update the timer's UILabel. So apparently I can't update both.
I have tried adding the Timer to RunLoop, using a DispatchQueue and creating a Timer that takes a selector but nothing works.
class ViewController: UIViewController {
let starImage = UIImageView(image: UIImage(systemName: "star.fill"))
let timerLabel = UILabel()
var counter = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupViews()
gameEngine()
}
private func setupViews() {
view.addSubview(starImage)
starImage.tintColor = .systemYellow
starImage.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(timerLabel)
timerLabel.tintColor = .white
timerLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
starImage.heightAnchor.constraint(equalToConstant: 100),
starImage.widthAnchor.constraint(equalToConstant: 100),
starImage.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
starImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timerLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
])
}
// PROBLEM HERE
private func gameEngine() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
self?.counter += 1
self?.timerLabel.text = "Timer: \(self!.counter)"
self?.starImage.center.y += 10
}
}
}
I got it to work by using UIView.animate as Rob suggested it in the comments.
I replaced the line where I updated the Y value with this:
UIView.animate(withDuration: 1, animations: {
[weak self] in
self?.starImage.transform = CGAffineTransform(translationX: 0, y: CGFloat(self!.counter*100))
})
There are 2 issues:
Instead
timerLabel.tintColor = .white you need to use timerLabel.textColor = .white
2.To move starImage you need update constraint:
class ViewController: UIViewController {
let starImage = UIImageView(image: UIImage(systemName: "star.fill"))
let timerLabel = UILabel()
var counter = 0
var topConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupViews()
gameEngine()
}
private func setupViews() {
view.addSubview(starImage)
starImage.tintColor = .systemYellow
starImage.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(timerLabel)
timerLabel.tintColor = .white
timerLabel.textColor = .white
timerLabel.translatesAutoresizingMaskIntoConstraints = false
topConstraint = starImage.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
NSLayoutConstraint.activate([
starImage.heightAnchor.constraint(equalToConstant: 100),
starImage.widthAnchor.constraint(equalToConstant: 100),
topConstraint!,
starImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timerLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
timerLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
])
}
// PROBLEM HERE
private func gameEngine() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
self?.counter += 1
self?.timerLabel.text = "Timer: \(self!.counter)"
self!.topConstraint?.constant += 10
}
}
}
So I'm trying to show an UIView in a stack view when I click continue on a controller and pop back to the previous controller. However, I'm having trouble showing the view when I pop. It will show if I present the controller, but I need it to show when I pop. How should I go about this? Thank you.
// ServiceDetailController
// MARK: - Properties
lazy var dateContainer: ShadowCardView = {
let view = ShadowCardView()
view.backgroundColor = .white
view.addShadow()
view.setHeight(height: 40)
let stack = UIStackView(arrangedSubviews: [calendarIcon, dateLabel, timeOfDayLabel])
stack.axis = .horizontal
stack.distribution = .fillProportionally
stack.spacing = 8
view.addSubview(stack)
stack.centerY(inView: view)
stack.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 12, paddingRight: 70)
view.addSubview(closeButton)
closeButton.centerY(inView: view)
closeButton.anchor(right: view.rightAnchor, paddingRight: 12)
return view
}()
lazy var dateStack = UIStackView(arrangedSubviews: [dateLabelStack, dateContainer, dateContainerView])
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Selectors
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
navigationController?.pushViewController(controller, animated: true)
}
// MARK: - Helper Functions
fileprivate func configureUI() {
setupNavigationBar()
view.backgroundColor = .groupTableViewBackground
setupServiceInfoView()
setupFormView()
}
fileprivate func setupFormView() {
showDataContainer(shouldShow: false)
setupTapGestureRecognizers()
}
fileprivate func setupTapGestureRecognizers() {
let dateTap = UITapGestureRecognizer(target: self, action: #selector(handleDateCreationTapped))
dateTap.numberOfTapsRequired = 1
dateTextField.addGestureRecognizer(dateTap)
}
func showDateContainer(shouldShow: Bool) {
if shouldShow {
dateContainer.isHidden = false
dateStack.spacing = 5
} else {
dateContainer.isHidden = true
dateStack.spacing = -18
}
}
// DateCreationController
// MARK: - Properties
private let continueButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Continue", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNext-Medium", size: 14)
button.backgroundColor = .darkGray
button.setHeight(height: 50)
button.layer.cornerRadius = 8
button.addTarget(self, action: #selector(handleContinue), for: .touchUpInside)
return button
}()
// MARK: - Selectors
#objc func handleContinue() {
let controller = ServiceDetailController()
controller.showDateContainer(shouldShow: true)
navigationController?.popViewController(animated: true)
}
The main problem is this func in your DateCreationController:
#objc func handleContinue() {
// here, you are creating a NEW instance of ServiceDetailController
let controller = ServiceDetailController()
controller.showDateContainer(shouldShow: true)
// here, you are popping back to where you came from... the EXISTING instance of ServiceDetailController
navigationController?.popViewController(animated: true)
}
You need to use either delegate/protocol pattern or a closure.
Here's an example using a closure...
Add this var to your DateCreationController class:
var continueCallback: ((Bool)->())?
In your ServiceDetailController class, when you instantiate and push your DateCreationController, you'll also setup that closure:
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
// add the closure
controller.continueCallback = { [weak self] shouldShow in
guard let self = self else { return }
self.showDateContainer(shouldShow: shouldShow)
self.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
Then, in your button action in DateCreationController, you "call back" using that closure:
#objc func handleContinue() {
continueCallback?(true)
}
Here's a runnable example. It creates a simple yellow view as the dateContainer view, but it is hidden on load. Tapping anywhere will push to DateCreationController, which has a single "Continue" button. Tapping that button will execute the closure, where the dateContainer view will be set to visible and we'll pop back:
class ServiceDetailController: UIViewController {
var jobService: String = "abc"
// plain yellow view
let dateContainer: UIView = {
let view = UIView()
view.backgroundColor = .yellow
return view
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
let infoLabel = UILabel()
infoLabel.text = "Tap anywhere"
view.addSubview(infoLabel)
view.addSubview(dateContainer)
infoLabel.translatesAutoresizingMaskIntoConstraints = false
dateContainer.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
dateContainer.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
dateContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dateContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dateContainer.heightAnchor.constraint(equalToConstant: 100.0),
])
setupTapGestureRecognizers()
// hide dateContainer on load
showDateContainer(shouldShow: false)
}
// MARK: - Selectors
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
// add the closure
controller.continueCallback = { [weak self] shouldShow in
guard let self = self else { return }
self.showDateContainer(shouldShow: shouldShow)
self.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
// MARK: - Helper Functions
fileprivate func setupTapGestureRecognizers() {
let dateTap = UITapGestureRecognizer(target: self, action: #selector(handleDateCreationTapped))
dateTap.numberOfTapsRequired = 1
view.addGestureRecognizer(dateTap)
}
func showDateContainer(shouldShow: Bool) {
if shouldShow {
dateContainer.isHidden = false
} else {
dateContainer.isHidden = true
}
}
}
class DateCreationController: UIViewController {
var jobService: String = "abc"
var continueCallback: ((Bool)->())?
lazy var continueButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Continue", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNext-Medium", size: 14)
button.backgroundColor = .darkGray
button.layer.cornerRadius = 8
button.addTarget(self, action: #selector(handleContinue), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
view.addSubview(continueButton)
continueButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
continueButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
continueButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
continueButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75),
continueButton.heightAnchor.constraint(equalToConstant: 50.0),
])
}
// MARK: - Selectors
#objc func handleContinue() {
continueCallback?(true)
}
}
I've been using various CIFilters to achieve a certain effect, but when narrowing down my problem I've found that CIHeightFieldFromMask yields no result; meaning the mask image looks exactly the same after I apply the filter. I'm using the same exact black and white text image used in the Apple Docs.
ciImage = ciImage
.applyingFilter("CIHeightFieldFromMask", parameters: [kCIInputImageKey: ciImage,
kCIInputRadiusKey: ShadedHeightMaskInputRadius])
ShadedHeightMaskInputRadius is a value that I can change ranging from 0 to 100 with a slider, so I've also tried all kinds of input radii with no difference. How can I achieve the same exact result shown in the documentation?
Not sure why your attempt is failing...
This might work for you:
extension UIImage {
func applyHeightMap(withRadius: Int) -> UIImage {
guard let thisCI = CIImage(image: self) else { return self }
let newCI = thisCI
.applyingFilter("CIHeightFieldFromMask",
parameters: [kCIInputRadiusKey: withRadius])
return UIImage(ciImage: newCI)
}
}
Usage would be:
let newImg = self.maskImg.applyHeightMap(withRadius: self.radius)
Here is some sample code to try it out. It has a 0 to 50 range slider for the radius. This is (as you probably know) very slow on a simulator, so this updates the image only when you release the slider:
class CITestViewController: UIViewController {
let theSlider: UISlider = {
let v = UISlider()
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.text = " "
return v
}()
let theImageView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.backgroundColor = .blue
return v
}()
let theSpinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView()
return v
}()
var maskImg: UIImage!
var radius: Int = 10
override func viewDidLoad() {
super.viewDidLoad()
guard let mImg = UIImage(named: "ci_heightmask") else {
fatalError("could not load ci_heightmask image")
}
maskImg = mImg
[theSlider, theLabel, theImageView, theSpinner].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
theLabel.topAnchor.constraint(equalTo: theSlider.bottomAnchor, constant: 20.0),
theLabel.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor, constant: 0.0),
theImageView.topAnchor.constraint(equalTo: theLabel.bottomAnchor, constant: 20.0),
theImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
theImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor),
theSpinner.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
theSpinner.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
])
theSlider.minimumValue = 0
theSlider.maximumValue = 1
theSlider.value = Float(radius) / 50.0
theSlider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
theSlider.addTarget(self, action: #selector(sliderReleased(_:)), for: .touchUpInside)
theLabel.text = "Radius: \(radius)"
updateImage()
}
#objc func sliderChanged(_ sender: Any) {
if let s = sender as? UISlider {
let v = Int((s.value * 50).rounded())
if radius != v {
radius = v
theLabel.text = "Radius: \(radius)"
}
}
}
#objc func sliderReleased(_ sender: Any) {
updateImage()
}
func updateImage() -> Void {
theSlider.isHidden = true
theSpinner.startAnimating()
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
let newImg = self.maskImg.applyHeightMap(withRadius: self.radius)
DispatchQueue.main.async {
self.theImageView.image = newImg
self.theSpinner.stopAnimating()
self.theSlider.isHidden = false
}
}
}
}
ci_heightmask.png
Results: