setting adjustsImageWhenHighlighted with UIButtonConfiguration - ios

Since adjustsImageWhenHighlighted is deprecated in iOS 15.0, what's the way to set it with UIButtonConfiguration? If I set it directly, I get a message saying "You may customize to replicate this behavior via a configurationUpdateHandler", but what exactly should I be doing to set this in configurationUpdateHandler? Thanks

This is the basics...
Assuming you have "normal" and "highlighted" images:
// let's use new UIButton.Configuration
newButton.configuration = .plain()
newButton.configurationUpdateHandler = { button in
var config = button.configuration
config?.image = button.isHighlighted ? imgHighlighted : imgNormal
button.configuration = config
}
Here's a runnable example, that uses UIGraphicsImageRenderer to create a "darkened" version of the button image "on-the-fly":
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let imgNormal = UIImage(named: "myButtonImage")
else {
print("Could not load images!")
return
}
// generate a "darkened" version of the image
let rndr = UIGraphicsImageRenderer(size: imgNormal.size)
let imgHighlighted = rndr.image { (ctx) in
imgNormal.draw(at: .zero)
UIColor.black.withAlphaComponent(0.5).setFill()
ctx.cgContext.fill(CGRect(origin: .zero, size: imgNormal.size))
}
let oldButton = UIButton()
let newButton = UIButton()
// "standard" button
oldButton.setImage(imgNormal, for: [])
// default is .adjustsImageWhenHighlighted = true
// let's use new UIButton.Configuration
newButton.configuration = .plain()
newButton.configurationUpdateHandler = { button in
var config = button.configuration
config?.image = button.isHighlighted ? imgHighlighted : imgNormal
button.configuration = config
}
[oldButton, newButton].forEach { b in
b.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(b)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
oldButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
oldButton.widthAnchor.constraint(equalToConstant: 80.0),
oldButton.heightAnchor.constraint(equalTo: oldButton.widthAnchor),
oldButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
newButton.topAnchor.constraint(equalTo: oldButton.bottomAnchor, constant: 20.0),
newButton.widthAnchor.constraint(equalTo: oldButton.widthAnchor),
newButton.heightAnchor.constraint(equalTo: oldButton.widthAnchor),
newButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
}
}
When you run this, the two buttons should look and function the same.

Related

Change the size of my component dynamically based on the amount of text in my label

I have a simple component, a label and an imageview.
They are put in horizontal position. I need to center that and if my label gets more text, the component will change too and the position of my image will change. I have to do this programmatically and I’m really new to programmatically constraints.
For more explanation this is an example:
And this is my code right now:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
progressText.sizeToFit()
progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_success")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView.image = resizedImage
iconView.sizeToFit()
iconView.layoutIfNeeded()
return iconView
}()
let iconView2: UIImageView = {
let iconView2 = UIImageView()
iconView2.translatesAutoresizingMaskIntoConstraints = false
let image = UIImage(named: "feedback_error")!
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
}
iconView2.image = resizedImage
iconView2.sizeToFit()
iconView2.layoutIfNeeded()
return iconView2
}()
private func setupView() {
let overlayView = UIView()
overlayView.translatesAutoresizingMaskIntoConstraints = false
addSubview(overlayView)
overlayView.addSubview(iconView)
overlayView.addSubview(iconView2)
NSLayoutConstraint.activate([
iconView.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
iconView.centerYAnchor.constraint(equalTo: iconView.centerYAnchor),
progressIndicator.centerXAnchor.constraint(equalTo: iconView2.centerXAnchor),
progressIndicator.centerYAnchor.constraint(equalTo: iconView2.centerYAnchor),
iconView2.topAnchor.constraint(equalTo: self.topAnchor, constant: 20),
iconView2.widthAnchor.constraint(equalToConstant: 10),
iconView2.heightAnchor.constraint(equalToConstant: 10),
iconView2.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: -10)
])
addSubview(progressText)
NSLayoutConstraint.activate([
progressText.leadingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: 15),
progressText.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Also, this is how I set this in a parent view:
private func setupProgressBar() {
self.codeValidationTextField.addSubview(progressBar)
progressBar.translatesAutoresizingMaskIntoConstraints = false
progressBar.leadingAnchor.constraint(equalTo: codeValidationTextField.leadingAnchor, constant: 10).isActive = true
progressBar.bottomAnchor.constraint(equalTo: codeValidationTextField.bottomAnchor, constant: -33).isActive = true
progressBar.widthAnchor.constraint(equalTo: codeValidationTextField.widthAnchor, multiplier: 0.5)
progressBar.progressIndicator.isHidden = false
progressBar.iconView.isHidden = true
progressBar.progressText.isHidden = false
}
It's not entirely clear what you are trying to do...
your code references a progressIndicator when setting constraints, but there is no progressIndicator identified.
there is no reason to be "scaling" your images... the image view will do that for you.
you're adding 2 "icon" image views, but the layout doesn't make much sense.
there is no need to use a overlayView -- we can add the subviews to the custom view itself (and, the code you've shown doesn't setup that overlayView correctly, so no idea if it is supposed to be doing anything).
Note that it helps to include comments in your code, telling yourself what you expect to happen.
And, a tip: during development, give your UI elements contrasting background colors to make it easy to see the framing.
So, let's simplify this and start with a single image view and label:
class PVProgressIndicator: UIView {
let progressText: UILabel = {
let progressText = UILabel()
progressText.font = UIFont.systemFont(ofSize: 11)
progressText.text = ""
progressText.numberOfLines = 0
progressText.textAlignment = .right
progressText.translatesAutoresizingMaskIntoConstraints = false
// no need for any of this
//progressText.sizeToFit()
//progressText.layoutIfNeeded()
return progressText
}()
let iconView: UIImageView = {
let iconView = UIImageView()
iconView.translatesAutoresizingMaskIntoConstraints = false
if let image = UIImage(named: "feedback_success") {
iconView.image = image
} else {
// could not load image, so set background color
iconView.backgroundColor = .systemRed
}
// no need for any of this
//let renderer = UIGraphicsImageRenderer(size: CGSize(width: 16, height: 16))
//let resizedImage = renderer.image { _ in
// image.draw(in: CGRect(origin: .zero, size: CGSize(width: 16, height: 16)))
//}
//iconView.image = resizedImage
//iconView.sizeToFit()
//iconView.layoutIfNeeded()
return iconView
}()
private func setupView() {
// no need for this... add subviews to self
//let overlayView = UIView()
//overlayView.translatesAutoresizingMaskIntoConstraints = false
//addSubview(overlayView)
self.addSubview(iconView)
self.addSubview(progressText)
NSLayoutConstraint.activate([
// 20-points on top and bottom of iconView
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 20.0),
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20.0),
// align iconView to leading-edge of self
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
// iconView 16 x 16
iconView.widthAnchor.constraint(equalToConstant: 16),
iconView.heightAnchor.constraint(equalToConstant: 16),
// progressText centered vertically
progressText.centerYAnchor.constraint(equalTo: self.centerYAnchor),
// progressText 15-points from iconView trailing edge
progressText.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 15.0),
// align progressText to trailing-edge of self
progressText.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
])
// during development, use some background colors so we can see the framing
progressText.backgroundColor = .cyan
self.backgroundColor = .systemGreen
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupView()
}
}
And an example view controller showing it. Each tap anywhere in the view will cycle through several "example" progress strings:
class PVTestViewController: UIViewController {
let progressBar = PVProgressIndicator()
let sampleStrings: [String] = [
"Short",
"Medium Length",
"A Longer Sample String",
"This one should be long enough that it will word wrap onto more than one line.",
"Let's make this sample string\nuse\n4 lines\nof text.",
"If the progress text might be very, very long... so long that it could wrap onto more than four lines (like this), then some additional constraints will need to be set on the elements in the progressBar view.",
]
var sampleIDX: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
progressBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressBar)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain progressBar Top 20-points from top of safe area
progressBar.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// centered horizontally
progressBar.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// let's set the max width to 75% of safe area width
progressBar.widthAnchor.constraint(lessThanOrEqualTo: g.widthAnchor, multiplier: 0.75),
// progressBar sets it's own height
])
updateProgress()
// instruction label
let v = UILabel()
v.text = "Tap anywhere to cycle through the sample progress strings."
v.numberOfLines = 0
v.textAlignment = .center
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
NSLayoutConstraint.activate([
v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
func updateProgress() {
progressBar.progressText.text = sampleStrings[sampleIDX % sampleStrings.count]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
sampleIDX += 1
updateProgress()
}
}
When you start, it will look like this:
Each tap cycles to the next sample string...

Weird horizontal shrinking animation when hiding UIButton with Configuration in UIStackView

I'm facing this weird animation issues when hiding UIButton in a StackView using the new iOS 15 Configuration. See playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()
Which results in this animation:
But I want the animation to look like this, where the button only shrinks vertically:
Which you can replicate in the playground by just swapping contentStackView.addArrangedSubview(makeConfigurationButton()) for contentStackView.addArrangedSubview(makeSystemButton()).
I guess this has something to do with the stack view alignment, setting it to center gives me the desired animation, but then the buttons don't fill the stack view width anymore and setting the width through AutoLayout results in the same animation again... Also, having just one system button in the stack view results in the same weird animation, but why does it behave differently for two system buttons? What would be a good solution for this problem?
As you've seen, the built-in show/hide animation with UIStackView can be quirky (lots of other quirks when you really get into it).
It appears that, when using a button with UIButton.Configuration, the button's width changes from the width assigned by the stack view to its intrinsic width as the animation occurs.
We can get around that by giving the button an explicit height constraint -- but, what if we want to use the intrinsic height (which may not be known in advance)?
Instead of setting the constraint, set the button's Content Compression Resistance Priority::
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
And we no longer get the horizontal sizing:
As you will notice, though, the button doesn't "squeeze" vertically... it gets "pushed up" outside the stack view's bounds.
We can avoid that by setting .clipsToBounds = true on the stack view:
If this effect is satisfactory, we're all set.
However, as we can see, the button is still not getting "squeezed." If that is the visual effect we want, we can use a custom "self-stylized" button instead of a Configuration button:
Of course, there is very little visual difference - and looking closely the button's text is not squeezing. If we really, really, really want that to happen, we need to animate a transform instead of using the stack view's default animation.
And... if we are taking advantage of some of the other conveniences with Configurations, using a self-stylized UIButton might not be an option.
If you want to play with the differences, here's some sample code:
class ViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
for i in 1...3 {
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
if i == 1 {
label.text = "Original Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeOrigConfigurationButton())
}
}
if i == 2 {
label.text = "Resist Compression Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeConfigurationButton())
}
}
if i == 3 {
label.text = "Custom Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeCustomButton())
}
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 100.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
}
// a horizontal stack view to hold a label and UISwitch
let ctbStack = UIStackView()
ctbStack.axis = .horizontal
ctbStack.spacing = 8
let label = UILabel()
label.text = "Clips to Bounds"
let ctbSwitch = UISwitch()
ctbSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
ctbStack.addArrangedSubview(label)
ctbStack.addArrangedSubview(ctbSwitch)
// put the label/switch stack in a view so we can center it
let ctbView = UIView()
ctbStack.translatesAutoresizingMaskIntoConstraints = false
ctbView.addSubview(ctbStack)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(ctbView)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
ctbStack.topAnchor.constraint(equalTo: ctbView.topAnchor),
ctbStack.bottomAnchor.constraint(equalTo: ctbView.bottomAnchor),
ctbStack.centerXAnchor.constraint(equalTo: ctbView.centerXAnchor),
])
}
#objc func switchChanged(_ sender: UISwitch) {
btnStacks.forEach { v in
v.clipsToBounds = sender.isOn
}
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[0].isHidden.toggle()
st.arrangedSubviews[0].alpha = st.arrangedSubviews[0].isHidden ? 0 : 1
}
}
}
}
private func makeOrigConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
}
private func makeCustomButton() -> UIButton {
let button = UIButton()
button.setTitle("Custom Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.lightGray, for: .highlighted)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 6
return button
}
}
Looks like this:
Edit
Quick example of another "quirk" when it comes to hiding a stack view's arranged subview (excess code in here, but I stripped down the above example):
class MyViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
let colors: [UIColor] = [
.cyan, .green, .yellow, .orange, .white
]
label.text = "Labels"
for j in 0..<colors.count {
let v = UILabel()
v.text = "Label"
v.textAlignment = .center
v.backgroundColor = colors[j]
if j == 2 {
v.text = "Height Constraint = 80.0"
v.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
}
st.addArrangedSubview(v)
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 300.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[2].isHidden.toggle()
}
}
}
}
}
When this is run and the "Toggle" button is tapped, it will be painfully obvious what's "not-quite-right."
You should add height constraint to buttons and update this constraint while animating. I edit your code just as below.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
toggleElement.heightAnchor.constraint(equalToConstant: toggleElement.isHidden ? 0 : 50)
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50)
])
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()

Trying to update local json file

To start off, I want to say that I am new to using JSON, so if this code needs some cleanup also, let me know. So I am trying to update json file. I have created an empty jsonfile named data.json in my project, inside a folder I named Data. The file looks like this:
{
"fullname": "Testuser2",
"answered": 0,
"correct": 0
}
This is my JSONData.swift:
import Foundation
struct UserData: Codable {
let fullname: String
let answered: Int
let correct: Int
}
struct GlobalVariable{
static var fullname = String()
static var answered = Int()
static var correct = Int()
}
These function to read the json file:
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parse(jsonData: Data) {
do {
let decodedData = try JSONDecoder().decode(UserData.self,
from: jsonData)
GlobalVariable.fullname = decodedData.fullname
GlobalVariable.answered = decodedData.answered
GlobalVariable.correct = decodedData.correct
} catch {
print("decode error")
}
}
And this is how I use it:
func getJSON() {
if let localData = self.readLocalFile(forName: "data") {
self.parse(jsonData: localData)
print(GlobalVariable.fullname)
print(GlobalVariable.answered)
print(GlobalVariable.correct)
}
}
So the getJSON successfully shows me the correct values.
Now after the GlobalVariable.answered and GlobalVariable.correct has been incrementet, I want to save it.
This is the code I am trying to use to save it:
func saveJSON {
print("---------")
print(GlobalVariable.fullname)
print(GlobalVariable.answered)
print(GlobalVariable.correct)
print("---------")
let dataToUpdate = [UserData(fullname: GlobalVariable.fullname, answered: GlobalVariable.answered, correct: GlobalVariable.correct)]
do {
let jsonData = try JSONEncoder().encode(dataToUpdate)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
// Handle error
}
}
} catch { print(error) }
}
The three print codes inside saveJSON shows the correct value, but someone nothing gets saved to the json file. The json file stays untouched.. How come?
I've also tried removing the .json from let pathWithFilename = documentDirectory.appendingPathComponent("data.json"), but still not working..
You are starting with your data.json in your bundle.
You read that, change the values, and then write it to Documents folder.
So, when you want to read the file, you need to first see if it exists in the Documents folder. If so, read it from there. If it doesn't exist (e.g. first time the app is run), then read it from the bundle.
Edit
The general idea is:
try to load the json file from the app's Documents folder
if that fails
create a "blank" user data object and show the Name Input view
if we successfully loaded the json file
use that data
after every change (name was entered, user answered a question), update the user data object and save (overwrite) the json file in the Documents folder
No need to have a .json file in your bundle, because you'll create a blank user data object if there is no .json file in the Documents folder.
Here is a complete example you can run and inspect:
struct UserData: Codable {
var fullname: String
var answered: Int
var correct: Int
}
class FileTestVC: UIViewController {
var userData: UserData!
let nameTextField = UITextField()
let nameLabel = UILabel()
let questionLabel = UILabel()
let resultNameLabel = UILabel()
let resultLabel = UILabel()
let userNameView = UIView()
let questionView = UIView()
let restartView = UIView()
let numQuestions: Int = 10
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupNameView()
setupQuestionView()
setupRestartView()
[userNameView, questionView, restartView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.layer.cornerRadius = 16
v.layer.borderWidth = 2
v.isHidden = true
view.addSubview(v)
}
userNameView.layer.borderColor = UIColor.systemBlue.cgColor
questionView.layer.borderColor = UIColor.systemRed.cgColor
restartView.layer.borderColor = UIColor.systemGreen.cgColor
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
userNameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
userNameView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
questionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
questionView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
restartView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
restartView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
userData = UserData(fullname: "", answered: 0, correct: 0)
if let jData = readLocalFile(forName: "data.json") {
do {
userData = try JSONDecoder().decode(UserData.self, from: jData)
} catch {
print(error)
}
}
if userData.fullname.isEmpty {
userNameView.isHidden = false
} else {
if userData.answered < numQuestions {
questionView.isHidden = false
nameLabel.text = userData.fullname
showNextQuestion()
} else {
showResult()
}
}
}
func setupNameView() {
// add a text field and "Save" button to userNameView
nameTextField.borderStyle = .roundedRect
let saveBtn = UIButton()
saveBtn.setTitle("Save", for: [])
saveBtn.setTitleColor(.white, for: .normal)
saveBtn.setTitleColor(.lightGray, for: .highlighted)
saveBtn.backgroundColor = .systemBlue
saveBtn.layer.cornerRadius = 8
[nameTextField, saveBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
userNameView.addSubview(v)
}
let g = userNameView
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
nameTextField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
nameTextField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
nameTextField.widthAnchor.constraint(equalToConstant: 280.0),
saveBtn.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20.0),
saveBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
saveBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
saveBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
saveBtn.addTarget(self, action: #selector(saveNameTapped(_:)), for: .touchUpInside)
}
func setupQuestionView() {
// add labels + Right and Wrong buttons to questionView
nameLabel.textAlignment = .center
nameLabel.backgroundColor = .yellow
nameLabel.text = "Name"
questionLabel.textAlignment = .center
questionLabel.backgroundColor = .green
questionLabel.numberOfLines = 0
questionLabel.text = "Question"
let rightBtn = UIButton()
rightBtn.setTitle("Right", for: [])
rightBtn.setTitleColor(.white, for: .normal)
rightBtn.setTitleColor(.lightGray, for: .highlighted)
rightBtn.backgroundColor = .systemGreen
rightBtn.layer.cornerRadius = 8
let wrongBtn = UIButton()
wrongBtn.setTitle("Wrong", for: [])
wrongBtn.setTitleColor(.white, for: .normal)
wrongBtn.setTitleColor(.lightGray, for: .highlighted)
wrongBtn.backgroundColor = .systemRed
wrongBtn.layer.cornerRadius = 8
[nameLabel, questionLabel, rightBtn, wrongBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
questionView.addSubview(v)
}
let g = questionView
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
nameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
nameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
questionLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20.0),
questionLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
questionLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
questionLabel.widthAnchor.constraint(equalToConstant: 280.0),
rightBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
rightBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
wrongBtn.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20.0),
wrongBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
wrongBtn.leadingAnchor.constraint(equalTo: rightBtn.trailingAnchor, constant: 20.0),
wrongBtn.widthAnchor.constraint(equalTo: rightBtn.widthAnchor),
wrongBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
rightBtn.addTarget(self, action: #selector(rightAnswerTapped(_:)), for: .touchUpInside)
wrongBtn.addTarget(self, action: #selector(wrongAnswerTapped(_:)), for: .touchUpInside)
}
func setupRestartView() {
// add labels + button to restartView
resultNameLabel.textAlignment = .center
resultNameLabel.backgroundColor = .yellow
resultNameLabel.text = "name"
resultLabel.textAlignment = .center
resultLabel.backgroundColor = .green
resultLabel.numberOfLines = 0
resultLabel.text = "You answered"
let restartBtn = UIButton()
restartBtn.setTitle("Restart", for: [])
restartBtn.setTitleColor(.white, for: .normal)
restartBtn.setTitleColor(.lightGray, for: .highlighted)
restartBtn.backgroundColor = .systemBlue
restartBtn.layer.cornerRadius = 8
[resultNameLabel, resultLabel, restartBtn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
restartView.addSubview(v)
}
let g = restartView
NSLayoutConstraint.activate([
resultNameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
resultNameLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultNameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
resultLabel.topAnchor.constraint(equalTo: resultNameLabel.bottomAnchor, constant: 20.0),
resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
resultLabel.widthAnchor.constraint(equalToConstant: 280.0),
restartBtn.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: 20.0),
restartBtn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
restartBtn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
restartBtn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
restartBtn.addTarget(self, action: #selector(restartTapped(_:)), for: .touchUpInside)
}
#objc func saveNameTapped(_ sender: Any?) {
guard let s = nameTextField.text,
!s.isEmpty
else { return }
view.endEditing(true)
userNameView.isHidden = true
questionView.isHidden = false
userData.fullname = s
nameLabel.text = userData.fullname
saveJSON(userData)
questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
}
#objc func restartTapped(_ sender: Any?) {
// "empty" user data object
userData = UserData(fullname: "", answered: 0, correct: 0)
// save (overwrite) existing data
saveJSON(userData)
restartView.isHidden = true
nameTextField.text = ""
userNameView.isHidden = false
}
#objc func rightAnswerTapped(_ sender: Any?) {
userData.correct += 1
showNextQuestion()
}
#objc func wrongAnswerTapped(_ sender: Any?) {
showNextQuestion()
}
func showNextQuestion() {
userData.answered += 1
saveJSON(userData)
if userData.answered < numQuestions {
questionLabel.text = "Question \(userData.answered + 1) of \(numQuestions)\nAnswer Right or Wrong"
} else {
questionView.isHidden = true
showResult()
}
}
func showResult() {
restartView.isHidden = false
resultNameLabel.text = userData.fullname
resultLabel.text = "You answered \(userData.correct) Right!"
}
private func readLocalFile(forName name: String) -> Data? {
if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent(name)
do {
let jd = try String(contentsOf: pathWithFilename)
return jd.data(using: .utf8)
} catch {
print(error)
}
return nil
}
return nil
}
func saveJSON(_ d: UserData) {
do {
let jsonData = try JSONEncoder().encode(d)
let jsonString = String(data: jsonData, encoding: .utf8)!
if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("data.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
print(error)
}
}
} catch {
print(error)
}
}
}

CIHeightFieldFromMask output image is the same as input

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:

How do I collapse UIViews programatically?

I'm working on my first IOS app and have run into an issue. I have a quite elaborate programmatic autoloyout UI that responds to user interaction. When a keyboard is shown certain Views must be collapsed, others moved and others spawned into existence based on a few conditions.
Now in it's default state no autolayout errors occur. But once things start moving it all comes apart. A few of the issues have to do with images retaining their height, while their view's heigconstriant is set to 0. Now I do have .scaleToFill enabled.
I have looked into stackViews however since most of my Views are of a different size with different nested UI elements stackviews do now appear to solve my issues. But I would certainly like some input on that.
Now my questions is: How do I collapse UIView and UIImageviews dynamically and programatically?
Now I don't mind typing out a lot of constraints manually, as long as it works.
Here are the constraints of the Views in question(there are more)
func setUpLayout() {
// SuggestionCloud
suggestionCloud.setConstraints(
topAnchor: textView.bottomAnchor, topConstant: 0,
bottomAnchor: bottomMenu.topAnchor, bottomConstant: 0,
trailingAnchor: view.trailingAnchor, trailingConstant: -10,
leadingAnchor: view.leadingAnchor, leadingConstant: 10)
print("Suggestion View frame :\(suggestionCloud.frame)")
//WEIGHT_IMAGE_VIEW
weigtImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 30).isActive = true
weigtImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
weigtImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
weigtImageView.heightAnchor.constraint(equalToConstant: 150).isActive = true
weigtImageView.addSubview(weightLabel);
print("Weight Image View \(weigtImageView.frame)")
//WEIGHT_LABEL
weightLabel.trailingAnchor.constraint(equalTo: weigtImageView.trailingAnchor, constant: -30).isActive = true;
weightLabel.leadingAnchor.constraint(equalTo: weigtImageView.leadingAnchor, constant: 25).isActive = true;
weightLabel.heightAnchor.constraint(equalTo: weigtImageView.heightAnchor, multiplier: 1).isActive = true;
//TEXT_VIEW
textView.topAnchor.constraint(equalTo: weigtImageView.bottomAnchor).isActive = true;
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true;
textView.heightAnchor.constraint(equalToConstant: 100).isActive = true;
textView.addSubview(nameTextField)
textView.addSubview(tagTextField)
textView.addSubview(setButtonView)
//TAG_CONTROLLER
tagController.heightAnchor.constraint(equalToConstant: 110).isActive = true;
tagController.topAnchor.constraint(equalTo: self.weigtImageView.bottomAnchor).isActive = true;
tagController.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant : 10).isActive = true
tagController.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10).isActive = true
//SET_BUTTON_VIEW
setButtonView.topAnchor.constraint(equalTo: textView.topAnchor).isActive = true;
setButtonView.bottomAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true;
setButtonView.trailingAnchor.constraint(equalTo: textView.trailingAnchor).isActive = true;
setButtonView.widthAnchor.constraint(equalToConstant: 110).isActive = true;
//NAME_TEXT_FIELD
nameTextField.trailingAnchor.constraint(equalTo: setButtonView.leadingAnchor, constant: -5).isActive = true
nameTextField.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 10).isActive = true
nameTextField.topAnchor.constraint(equalTo: textView.topAnchor, constant: 13).isActive = true
nameTextField.heightAnchor.constraint(equalToConstant: 31).isActive = true
nameTextField.layer.cornerRadius = 8
nameTextField.backgroundColor = .white;
//TAG_TEXT_FIELD
tagTextField.trailingAnchor.constraint(equalTo: setButtonView.leadingAnchor, constant: -5).isActive = true
tagTextField.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 10).isActive = true
tagTextField.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: -13).isActive = true
tagTextField.heightAnchor.constraint(equalToConstant: 31).isActive = true
tagTextField.layer.cornerRadius = 8
tagTextField.backgroundColor = .white
here's the viewcontrollers setup:
class UIScaleControllerVew: UIViewController, UITextFieldDelegate, SuggenstionCloudDelegate {
let weigtImageView : UIImageView = {
var imageView = UIImageView(image: UIImage(named: "scaleVisorShadow"));
imageView.contentMode = .scaleToFill
imageView.translatesAutoresizingMaskIntoConstraints = false;
return imageView
}()
let weightLabel : UILabel = {
let label = UILabel()
label.text = "135 gr"
label.font = UIFont(name: "Avenir-Light", size: 50.0)
label.textAlignment = .right
label.translatesAutoresizingMaskIntoConstraints = false
return label
}();
let textView : UIView = {
var view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false;
return view;
}();
let setButtonView : UIImageView = {
var imageView = UIImageView(image: UIImage(named: "setButton"))
imageView.translatesAutoresizingMaskIntoConstraints = false;
return imageView;
}();
let nameTextField : UITextField = {
var textField = UITextField();
textField.tag = 2;
textField.translatesAutoresizingMaskIntoConstraints = false;
textField.addTarget(self, action: #selector(nameFieldEditingChanged(_:)), for: UIControl.Event.editingChanged)
return textField;
}();
let tagTextField : UITextField = {
var textField = UITextField();
textField.tag = 1;
textField.translatesAutoresizingMaskIntoConstraints = false;
textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: UIControl.Event.editingChanged)
return textField;
}();
let bottomMenu : UIView = {
var view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false;
return view;
}();
let saveButton : UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "save"), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false;
return button
}();
let microPhoneButton : UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "microPhone"), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false;
return button;
}();
let suggestionCloud : SuggenstionCloud = {
let cloud = SuggenstionCloud(image: UIImage(named: "suggestionCloud.png"))
cloud.translatesAutoresizingMaskIntoConstraints = false;
return cloud;
}();
let tagController : TagController = {
let tagController = TagController()
tagController.translatesAutoresizingMaskIntoConstraints = false
return tagController;
}()
let scaleModel = ScaleModel.init()
override func viewDidLoad() {
super.viewDidLoad()
print("UIScaleController_DidLoad")
tagTextField.delegate = self
nameTextField.delegate = self;
suggestionCloud.delegate = self;
view.backgroundColor = UIColor(hexString: "8ED7F5")
view.addSubview(weigtImageView)
view.addSubview(textView)
view.addSubview(bottomMenu);
view.addSubview(suggestionCloud)
view.addSubview(tagController)
tagController.isHidden = true;
setUpLayout()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
var didSetUpSuggestionCloud = false
var didSetUpTagController = false
override func viewDidLayoutSubviews() {
guard !self.didSetUpTagController else {
return
}
guard !self.didSetUpSuggestionCloud else {
return
}
self.didSetUpSuggestionCloud = true
self.didSetUpTagController = true
};
and here's the problematic code:
#objc func keyboardWillShowNotification(notification: Notification ) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
// collapse and hide bottom view
bottomMenu.contentMode = .scaleToFill;
bottomMenu.heightAnchor.constraint(equalToConstant: 0).isActive = true;
bottomMenu.isHidden = true
// collapse and hide top view
weigtImageView.contentMode = .scaleToFill;
weigtImageView.heightAnchor.constraint(equalToConstant: 0).isActive = true;
weigtImageView.isHidden = true;
// spawn my tag view
tagController.topAnchor.constraint(equalTo: self.textView.bottomAnchor).isActive = true;
tagController.bottomAnchor.constraint(equalTo: suggestionCloud.topAnchor).isActive = true
tagController.isHidden = false;
// set textviews new constraints
textView.bottomAnchor.constraint(equalTo: tagController.topAnchor).isActive = true;
// set middleView's new constraints
suggestionCloud.topAnchor.constraint(equalTo: tagController.bottomAnchor).isActive = true;
suggestionCloud.bottomAnchor.constraint(equalTo: bottomMenu.topAnchor, constant: -keyboardSize.height).isActive = true
self.view.layoutIfNeeded()
}
}
Now there are so many unexpected things happening that i'm positive that my approach to this is just wrong conceptually.
Please let me know where I need to look for a solution.
Here are e few pictures of what is happening so far:
So when the keyboard is up:
The weightView is collapsed: suggestioncloud and text are moved up.
If a tag is added a new view called tagController needs to be places between the texView and the suggesitonCloud. Lastyl the keybaord needs to be collapsed again.
Ill add some sscreenshots
One thing that you could look at is duplicate constraints.
Every time you call weigtImageView.heightAnchor.constraint(equalToConstant: 0).isActive = true you are creating a new constraint. This does not automatically replace any previous height constraints that are active.
To replace a constraint, you need to keep a reference to it, deactivate it, and then activate a new one (and optionally assign it the the variable you use to keep a reference).
Stack views
Stack views could be helpful in your situation, because they automatically collapse views that have isHidden set to true. I think that as long as the direct subviews of the StackView have an intrinsic content size (e.g. correct internal constraints), they should be placed correctly by the StackView.
If you do not have a strong reference to your views to be released then it is enough to execute this:
if view2Breleased.superview != nil {
view2Breleased.removeFromSuperview()
}
The view will then disappear and be released from memory.
If you don't know what a strong reference is then for the time being, just try the code I wrote. The views will disappear anyway.
(Strong reference means you have assigned the view to a variable which survives the execution of the code view2Breleased.removeFromSuperview() and the exit from the function call where the code view2Breleased.removeFromSuperview() is.)
You can change the constant of the heightAnchor constraint like so:
import Foundation
import UIKit
class TestController : UIViewController {
var myViewHeightConstraint : NSLayoutConstraint!
let myView : UIControl = {
let newView = UIControl()
newView.translatesAutoresizingMaskIntoConstraints = false
newView.backgroundColor = .red
return newView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
self.myView.addTarget(self, action: #selector(viewClicked), for: .touchUpInside)
self.myViewHeightConstraint = myView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor)
setup()
}
func setup(){
view.addSubview(myView)
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
myView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
self.myViewHeightConstraint.isActive = true
}
#objc func viewClicked() {
self.myViewHeightConstraint.constant = -self.myView.frame.size.height
}
}
In my example, I set the constant to minus the height of the view's frame height, which effectively collapses it.

Resources