I have a screen that contains some content, and on the bottom of it I'd like to have a UITablView and I'd like that the two were synchronized. If I scroll the UITableView on the bottom, the content that's above the UIView would scroll as well, kinda like a sticky header but different in the sense that the entire screen could be filled by the UITableView.
How it is right now (the image on the bottom is simply me trying to add the element below):
How it should be - notice the list on the bottom, we can keep scrolling it.
This is my code right now:
//
// HomeViewController.swift
// FeatureExplorer
//
// Created by Rodrigo Vieira on 10/04/22.
//
import UIKit
import Combine
class HomeViewController: UIViewController {
var networkService = NetworkService.shared
var observers = [AnyCancellable]()
var rootScrollView: UIScrollView = {
var sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
var rootStackView: UIStackView = {
var rc = UIStackView()
rc.translatesAutoresizingMaskIntoConstraints = false
return rc
}()
var welcomeLabel: UILabel = {
let label = UILabel()
label.text = "Welcome!"
label.font = UIFont.boldSystemFont(ofSize: 20)
return label
}()
var profileImageView: UIImageView = {
let imageView = UIImageView(image: HomePageImages.profileIconImage!)
imageView.contentMode = .scaleAspectFit
return imageView
}()
var searchBar: UITextField = {
let textField = UITextField()
var leftPaddingView = UIView(
frame: CGRect(x: 0, y: 0, width: 10, height: 0)
)
textField.leftView = leftPaddingView
textField.leftViewMode = .always
textField.layer.cornerRadius = 12
textField.backgroundColor = .white
textField.placeholder = "Search cocktails"
textField.layer.shadowColor = UIColor.black.cgColor
textField.layer.shadowOffset = .init(width: 1, height: 1)
textField.layer.shadowRadius = 8
textField.layer.shadowOpacity = 0.2
return textField
}()
var screenContentContainerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 30
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.3
view.layer.shadowOffset = CGSize(width: 1, height: 1)
view.layer.shadowRadius = 12
return view
}()
var latestDrinksLabel: UILabel = {
let label = UILabel()
label.text = "Last cocktails published"
label.font = UIFont.boldSystemFont(ofSize: 18)
return label
}()
var recentDrinksCollectionView: RecentDrinksCollectionView = {
let collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.scrollDirection = .horizontal
let collectionView = RecentDrinksCollectionView(
collectionViewLayout: collectionViewLayout
)
return collectionView
}()
var popularCocktailsLabel: UILabel = {
var label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 18)
label.text = "Popular drinks"
return label
}()
override func viewDidLayoutSubviews() {
addScrollViewLayout()
addRootStackViewLayout()
addWelcomeLabelLayout()
addSearchBarLayout()
addScreenContentViewLayout()
addLatestLabelLayout()
addRecentDrinksCollectionLayout()
addPopularLabelLayout()
addPopularDrinksTableViewLayout()
}
override func viewDidLoad() {
super.viewDidLoad()
fetchAndDisplayPopularCocktails()
view.backgroundColor = Colors.mainRedColor
}
func addScrollViewLayout() {
view.addSubview(rootScrollView)
NSLayoutConstraint.activate([
rootScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
rootScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
rootScrollView.topAnchor.constraint(equalTo: view.topAnchor),
rootScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
func addRootStackViewLayout() {
rootScrollView.addSubview(rootStackView)
NSLayoutConstraint.activate([
rootStackView.leadingAnchor.constraint(
equalTo: rootScrollView.contentLayoutGuide.leadingAnchor
),
rootStackView.trailingAnchor.constraint(
equalTo: rootScrollView.contentLayoutGuide.trailingAnchor
),
rootStackView.topAnchor.constraint(
equalTo: rootScrollView.contentLayoutGuide.topAnchor
),
rootStackView.bottomAnchor.constraint(
equalTo: rootScrollView.contentLayoutGuide.bottomAnchor
),
rootStackView.widthAnchor.constraint(
equalTo: rootScrollView.frameLayoutGuide.widthAnchor
)
])
}
func addWelcomeLabelLayout() {
welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
profileImageView.translatesAutoresizingMaskIntoConstraints = false
rootStackView.addSubview(profileImageView)
rootStackView.addSubview(welcomeLabel)
NSLayoutConstraint.activate([
welcomeLabel.leadingAnchor.constraint(
equalTo: rootStackView.leadingAnchor,
constant: 20
),
welcomeLabel.trailingAnchor.constraint(
equalTo: rootStackView.trailingAnchor,
constant: -20
),
welcomeLabel.topAnchor.constraint(
equalTo: rootStackView.topAnchor,
constant: 20
),
profileImageView.topAnchor.constraint(
equalTo: rootStackView.topAnchor, constant: 20
),
profileImageView.centerXAnchor.constraint(
equalTo: rootStackView.trailingAnchor,
constant: -50
),
profileImageView.heightAnchor.constraint(equalToConstant: 60),
welcomeLabel.centerYAnchor.constraint(
equalTo: profileImageView.centerYAnchor
),
])
}
func addSearchBarLayout() {
searchBar.translatesAutoresizingMaskIntoConstraints = false
rootStackView.addSubview(searchBar)
NSLayoutConstraint.activate([
searchBar.leadingAnchor.constraint(
equalTo: rootStackView.leadingAnchor, constant: 20
),
searchBar.trailingAnchor.constraint(
equalTo: rootStackView.trailingAnchor, constant: -20
),
searchBar.topAnchor.constraint(
equalTo: profileImageView.bottomAnchor, constant: 20
),
searchBar.heightAnchor.constraint(equalToConstant: 50),
])
}
func addScreenContentViewLayout() {
screenContentContainerView.translatesAutoresizingMaskIntoConstraints = false
rootStackView.addSubview(screenContentContainerView)
NSLayoutConstraint.activate([
screenContentContainerView.leadingAnchor.constraint(
equalTo: rootStackView.leadingAnchor
),
screenContentContainerView.trailingAnchor.constraint(
equalTo: rootStackView.trailingAnchor
),
screenContentContainerView.bottomAnchor.constraint(
equalTo: view.bottomAnchor
),
screenContentContainerView.topAnchor.constraint(
equalTo: searchBar.bottomAnchor, constant: 25
)
])
}
func addLatestLabelLayout() {
latestDrinksLabel.translatesAutoresizingMaskIntoConstraints = false
rootStackView.addSubview(latestDrinksLabel)
NSLayoutConstraint.activate([
latestDrinksLabel.leadingAnchor.constraint(
equalTo: view.leadingAnchor, constant: 20
),
latestDrinksLabel.trailingAnchor.constraint(
equalTo: view.trailingAnchor, constant: -20
),
latestDrinksLabel.topAnchor.constraint(
equalTo: screenContentContainerView.topAnchor, constant: 30
)
])
}
func addRecentDrinksCollectionLayout() {
addChild(recentDrinksCollectionView)
recentDrinksCollectionView
.view
.translatesAutoresizingMaskIntoConstraints = false
screenContentContainerView.addSubview(recentDrinksCollectionView.view)
NSLayoutConstraint.activate([
recentDrinksCollectionView.view.topAnchor.constraint(
equalTo: latestDrinksLabel.bottomAnchor,
constant: 20
),
recentDrinksCollectionView.view.leadingAnchor.constraint(
equalTo: view.leadingAnchor
),
recentDrinksCollectionView.view.trailingAnchor.constraint(
equalTo: view.trailingAnchor
),
recentDrinksCollectionView.view.heightAnchor.constraint(
equalToConstant: 280
)
])
}
func addPopularLabelLayout() {
popularCocktailsLabel.translatesAutoresizingMaskIntoConstraints = false
screenContentContainerView.addSubview(popularCocktailsLabel)
NSLayoutConstraint.activate([
popularCocktailsLabel.topAnchor.constraint(
equalTo: recentDrinksCollectionView.view.bottomAnchor,
constant: 20
),
popularCocktailsLabel.leadingAnchor.constraint(
equalTo: rootStackView.leadingAnchor,
constant: 20
),
popularCocktailsLabel.trailingAnchor.constraint(
equalTo: rootStackView.trailingAnchor,
constant: -20
),
])
}
func addPopularDrinksTableViewLayout() {
}
func displayTwoFirstPopularDrinks(drinks: [DrinkModel]) {
let firstPopularDrinkView = PopularDrinkView()
rootStackView.addSubview(firstPopularDrinkView)
NSLayoutConstraint.activate([
firstPopularDrinkView.topAnchor.constraint(
equalTo: popularCocktailsLabel.bottomAnchor,
constant: 50
),
firstPopularDrinkView.leadingAnchor.constraint(
equalTo: rootStackView.leadingAnchor,
constant: 20
),
firstPopularDrinkView.trailingAnchor.constraint(
equalTo: rootStackView.trailingAnchor,
constant: -20
),
firstPopularDrinkView.heightAnchor.constraint(
equalToConstant: 300
),
firstPopularDrinkView.bottomAnchor.constraint(
equalTo: rootStackView.bottomAnchor
)
])
// firstPopularDrinkView.addContentToView(drink: drinks.first!)
}
func fetchAndDisplayPopularCocktails() {
networkService
.fetchPopularCocktails()
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("finished")
}
} receiveValue: { [weak self] value in
self?.displayTwoFirstPopularDrinks(drinks: value)
}
.store(in: &observers)
}
}
Thanks a lot for helping out, really!
As #son mentioned, I would suggest to go with a tableView or even better with a collectionView instead of a scroll view and split the UI into different cells.
But if adding a scrollView is what you want then try the following.
Disable the scroll on the tableView.
Add height to the tableView with 255 priority.
Add tableView.sizeToFit().
Related
I have been unable to figure out how to solve this issue of mine. I tried to follow the answer from this post. How to change UIView height based on elements inside it
Like the post answer says to do, I have:
set autolayout constraints between UIContainerView top to the UITextView top and UIContainerView bottom to the UITextView bottom (#1)
set height constraint on the text view (#2) and change its constant when resizing the text view (#3)
I have to do this all programmatically. I first set the frame for the container view and give it a specified height. I'm not sure if that is okay too. I also add (#1) in viewDidLoad and am unsure if that's correct.
The text view is not able to increase height either with the current constraints (it is able to if I remove the topAnchor constraint but the container view still doesn't change size).
class ChatController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
lazy var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height * 0.075)
return containerView
}()
lazy var textView: UITextView = {
let textView = UITextView()
textView.text = "Enter message..."
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.delegate = self
return textView
}()
override func viewDidLoad() {
super.viewDidLoad()
...
textViewDidChange(self.textView)
addContainerSubViews()
(#1)
containerView.topAnchor.constraint(equalTo: self.textView.topAnchor, constant: -UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.textView.bottomAnchor, constant: UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
}
func addContainerSubViews() {
let height = UIScreen.main.bounds.size.height
let width = UIScreen.main.bounds.size.width
let containerHeight = height * 0.075
...//constraints for imageView and sendButton...
containerView.addSubview(self.textView)
self.textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: width/20).isActive = true
self.textView.rightAnchor.constraint(equalTo: sendButton.leftAnchor, constant: -width/20).isActive = true
(#2)
self.textView.heightAnchor.constraint(equalToConstant: containerHeight * 0.6).isActive = true
}
override var inputAccessoryView: UIView? {
get {
return containerView
}
}
(#3)
func textViewDidChange(_ textView: UITextView) {
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
You can do this all with auto-layout / constraints. Because a UITextView with scrolling disabled will "auto-size" its height based on the text, no need to calculate height and change constraint constant.
Here's an example -- it's from a previous answer, modified to include your image view and send button:
class ViewController: UIViewController {
let testLabel: InputLabel = InputLabel()
override func viewDidLoad() {
super.viewDidLoad()
let instructionLabel = UILabel()
instructionLabel.textAlignment = .center
instructionLabel.text = "Tap yellow label to edit..."
let centeringFrameView = UIView()
// label properties
let fnt: UIFont = .systemFont(ofSize: 32.0)
testLabel.isUserInteractionEnabled = true
testLabel.font = fnt
testLabel.adjustsFontSizeToFitWidth = true
testLabel.minimumScaleFactor = 0.25
testLabel.numberOfLines = 2
testLabel.setContentHuggingPriority(.required, for: .vertical)
let minLabelHeight = ceil(fnt.lineHeight)
// so we can see the frames
centeringFrameView.backgroundColor = .red
testLabel.backgroundColor = .yellow
[centeringFrameView, instructionLabel, testLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(instructionLabel)
view.addSubview(centeringFrameView)
centeringFrameView.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// instruction label centered at top
instructionLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
instructionLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// centeringFrameView 20-pts from instructionLabel bottom
centeringFrameView.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 20.0),
// Leading / Trailing with 20-pts "padding"
centeringFrameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
centeringFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// test label centered vertically in centeringFrameView
testLabel.centerYAnchor.constraint(equalTo: centeringFrameView.centerYAnchor, constant: 0.0),
// Leading / Trailing with 20-pts "padding"
testLabel.leadingAnchor.constraint(equalTo: centeringFrameView.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: centeringFrameView.trailingAnchor, constant: -20.0),
// height will be zero if label has no text,
// so give it a min height of one line
testLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: minLabelHeight),
// centeringFrameView height = 3 * minLabelHeight
centeringFrameView.heightAnchor.constraint(equalToConstant: minLabelHeight * 3.0)
])
// to handle user input
testLabel.editCallBack = { [weak self] str in
guard let self = self else { return }
self.testLabel.text = str
}
testLabel.doneCallBack = { [weak self] in
guard let self = self else { return }
// do something when user taps done / enter
}
let t = UITapGestureRecognizer(target: self, action: #selector(self.labelTapped(_:)))
testLabel.addGestureRecognizer(t)
}
#objc func labelTapped(_ g: UITapGestureRecognizer) -> Void {
testLabel.becomeFirstResponder()
testLabel.inputContainerView.theTextView.text = testLabel.text
testLabel.inputContainerView.theTextView.becomeFirstResponder()
}
}
class InputLabel: UILabel {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
override var canBecomeFirstResponder: Bool {
return true
}
override var canResignFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
lazy var inputContainerView: CustomInputAccessoryView = {
let v = CustomInputAccessoryView()
v.editCallBack = { [weak self] str in
guard let self = self else { return }
self.editCallBack?(str)
}
v.doneCallBack = { [weak self] in
guard let self = self else { return }
self.resignFirstResponder()
}
return v
}()
}
class CustomInputAccessoryView: UIView, UITextViewDelegate {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
let theTextView: UITextView = {
let tv = UITextView()
tv.isScrollEnabled = false
tv.font = .systemFont(ofSize: 16)
tv.autocorrectionType = .no
tv.returnKeyType = .done
return tv
}()
let imgView: UIImageView = {
let v = UIImageView()
v.contentMode = .scaleAspectFit
v.clipsToBounds = true
return v
}()
let sendButton: UIButton = {
let v = UIButton()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .lightGray
autoresizingMask = [.flexibleHeight, .flexibleWidth]
if let img = UIImage(named: "testImage") {
imgView.image = img
} else {
imgView.backgroundColor = .systemBlue
}
let largeConfig = UIImage.SymbolConfiguration(pointSize: 22, weight: .regular, scale: .large)
let buttonImg = UIImage(systemName: "paperplane.fill", withConfiguration: largeConfig)
sendButton.setImage(buttonImg, for: .normal)
[theTextView, imgView, sendButton].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
}
// if we want to see the image view and button frames
//[imgView, sendButton].forEach { v in
// v.backgroundColor = .systemYellow
//}
NSLayoutConstraint.activate([
// constrain image view 40x40 with 8-pts leading
imgView.widthAnchor.constraint(equalToConstant: 40.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
// constrain image view 40x40 with 8-pts trailing
sendButton.widthAnchor.constraint(equalToConstant: 40.0),
sendButton.heightAnchor.constraint(equalTo: sendButton.widthAnchor),
sendButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
// constrain text view with 10-pts from
// image view trailing
// send button leading
theTextView.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10),
theTextView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10),
// constrain image view and button
// centered vertically
// at least 8-pts top and bottom
imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
sendButton.centerYAnchor.constraint(equalTo: centerYAnchor),
imgView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
sendButton.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
imgView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
sendButton.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
// constrain text view 8-pts top/bottom
theTextView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
theTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
theTextView.delegate = self
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
doneCallBack?()
}
return true
}
func textViewDidChange(_ textView: UITextView) {
editCallBack?(textView.text ?? "")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}
Output:
in my project I have a someView(of type UIView), that inside holderView(of type UIView).
someView have 2 state.
in state 1 the someView become large and in the step 2 the someView become small.
when some condition where right, someView show in state 1 and when it's not someView show in state 2.
I want to do this with animation and I use NSLayoutConstraint in my project. I'm using this codes(someViewHeight and someViewWidth are of type NSLayoutConstraint):
func minimizeSomeView() {
someViewHeight.constant = holderView.frame.width
someViewWidth.constant = holderView.frame.height
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
someViewHeight.constant = holderView.frame.width/4
someViewWidth.constant = holderView.frame.height/4
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
if someTextField.text != nil {
minimizeSomeView()
} else. {
maximizeSomeView()
}
I define someViewHeight and someViewWidth inside viewDidLayoutSubviews(), this is my codes:
class ViewController: UIViewController {
private var holderView, someView: UIView!
private var someViewHeight,someViewWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
super.viewDidLoad()
view.backgroundColor = .white
// Holder View
holderView = UIView()
holderView.backgroundColor = .red
holderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holderView)
NSLayoutConstraint.activate([
holderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
holderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
holderView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
holderView.heightAnchor.constraint(equalToConstant: view.frame.height * 40 / 100)
])
// Some View
someView = UIView()
someView.backgroundColor = .blue
someView.translatesAutoresizingMaskIntoConstraints = false
holderView.addSubview(someView)
someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
// Some TextField
let someTextField = UITextField()
someTextField.backgroundColor = .yellow
someTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(someTextField)
NSLayoutConstraint.activate([
someTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
someTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
someTextField.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -100),
someTextField.heightAnchor.constraint(equalToConstant: 50)
])
someTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
override func viewDidLayoutSubviews() {
someViewHeight = someView.heightAnchor.constraint(equalToConstant: holderView.frame.width)
someViewHeight.isActive = true
someViewWidth = someView.widthAnchor.constraint(equalToConstant: holderView.frame.width)
someViewWidth.isActive = true
}
func minimizeSomeView() {
someViewHeight.constant = holderView.frame.width/4
someViewWidth.constant = holderView.frame.height/4
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
someViewHeight.constant = holderView.frame.width
someViewWidth.constant = holderView.frame.height
UIView.animate(withDuration: 1.0) {
self.view.layoutIfNeeded()
}
}
#objc func textFieldDidChange(_ textfield: UITextField) {
if textfield.text!.count > 0 {
self.minimizeSomeView()
} else {
self.maximizeSomeView()
}
}
}
You should not update the constraints in viewDidLayoutSubviews, specifically when you are using auto layout. You don't need this block
When you add constraint you can initialize and make them active.
Here we can specify the constraint in proportion. i.e Height of someView is 0.4 of the holderView. Similarly, Width of someView is 0.4 of the holderView.
To fix the issue , you can perform below changes
// Your some View Constraints
someView.topAnchor.constraint(equalTo: holderView.topAnchor).isActive = true
someView.trailingAnchor.constraint(equalTo: holderView.trailingAnchor).isActive = true
maximizeSomeView()
Remove the viewDidLayouSubView Code
Update your maximize and minimize events. Here I have to remove the existing constraints as .multiplier is a readonly property.
func minimizeSomeView() {
removeExistingConstriant()
UIView.animate(withDuration: 1.0) { [unowned self] in
self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 1.0)
self.someViewWidth.isActive = true
self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
self.holderView.heightAnchor, multiplier: 1.0)
self.someViewHeight.isActive = true
self.view.layoutIfNeeded()
}
}
func maximizeSomeView() {
removeExistingConstriant()
UIView.animate(withDuration: 1.0) { [unowned self] in
self.someViewWidth = self.someView.widthAnchor.constraint(equalTo: self.holderView.widthAnchor, multiplier: 0.4)
self.someViewWidth.isActive = true
self.someViewHeight = self.someView.heightAnchor.constraint(equalTo:
self.holderView.heightAnchor, multiplier: 0.4)
self.someViewHeight.isActive = true
self.view.layoutIfNeeded()
}
}
func removeExistingConstriant(){
if self.someViewHeight != nil {
self.someViewHeight.isActive = false
}
if self.someViewWidth != nil {
self.someViewWidth.isActive = false
}
}
I'm relatively new to programming in general, and I'm only starting to pickup constraints and auto-layout and how to set things up properly. Currently, what I'm trying to figure out is how to make the top most UITextfield with the UILabel inside of it (The one that says Weight(lbs): 70.0) align to the specs that I have configured for it (See code bellow...).
When I specify the size of the width and length anchors, I always get a View that ignores my size specs and anchors to the entire screen (see image bellow).
Any help would be greatly appreciated.
import UIKit
import RealmSwift
class ThirdViewController: UIViewController {
let realm = try! Realm()
var stats : Results<WeightSetsReps>?
var weightTextField = UITextField()
var weightLabel = UILabel()
var repsTextField = UITextField()
var repsLabel = UILabel()
var timerImage = UIImageView()
var selectedExercise : Exercises? {
didSet{
loadWsr()
}
}
//MARK: - ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// timeClock()
navConAcc()
labelConfig()
setTextFieldConstraints()
// setImageViewConstraints()
}
//MARK: - UILabel
func labelConfig(){
weightTextField.placeholder = "Total weight..."
weightTextField.layer.borderWidth = 1
weightTextField.backgroundColor = .white
weightTextField.layer.cornerRadius = 25
weightTextField.layer.borderColor = UIColor.lightGray.cgColor
weightLabel.text = " Weight (lbs): "
weightLabel.textColor = .black
weightTextField.leftView = weightLabel
weightTextField.leftViewMode = .always
repsTextField.placeholder = "Number of Reps..."
repsTextField.layer.borderWidth = 1
repsTextField.backgroundColor = .white
repsTextField.layer.cornerRadius = 25
repsTextField.layer.borderColor = UIColor.lightGray.cgColor
repsLabel.text = " Repetitions: "
repsLabel.textColor = .black
repsTextField.leftView = repsLabel
repsTextField.leftViewMode = .always
[weightTextField, repsTextField].forEach{view.addSubview($0)}
}
//MARK: - TextField Constrainst
func setTextFieldConstraints(){
weightTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: view.trailingAnchor, size: .init(width: 20, height: 20))
repsTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: view.trailingAnchor, size: .init(width: 20, height: 20))
}
//MARK: - ImageView Constraints
// func setImageViewConstraints(){
//
// timerImage.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: view.trailingAnchor, padding: .init(top: 40, left: 40, bottom: 450, right: 50))
//
// }
//MARK: - Navigation Bar Setup
func navConAcc(){
navigationItem.title = selectedExercise?.exerciseName
navigationController?.navigationBar.prefersLargeTitles = true
}
//MARK: - Stopwatch
// func timeClock(){
// let image1 = UIImage(named: "stopwatch")
// timerImage = UIImageView(image: image1)
// timerImage.contentMode = .scaleAspectFit
// self.view.addSubview(timerImage)
// }
//MARK: - Load Data
func loadWsr() {
stats = selectedExercise?.wsr.sorted(byKeyPath: "sets", ascending: true)
}
//MARK: - Save Data
func save(wsr : WeightSetsReps){
do {
try realm.write {
realm.add(wsr)
}
} catch {
print("Error saving wsr data \(error)")
}
}
}
extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero){
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing, constant: padding.right).isActive = true
}
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
}
You need to remove bottom anchor
repsTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom:nil, trailing: view.trailingAnchor, size: .init(width: 20, height: 20))
My swift code below has 2 different buttons which should effect the slider baised on the .tagnumber they initialize. B1 should effect the alhpa or transpency of the imageivew and b2 should decrease / increase the size. What b2 does works. What B1 does not work.My code does not use any storyboards. Also the uislider should only do one task it cannont resize the imageview and change the alpha at the same time. Only 1 task.
import UIKit
class ViewController: UIViewController {
var pzc = UIImageView()
var s = UISlider()
var b1 = UIButton()
var b2 = UIButton()
var jessicaAlba:Float = 50
var topConstraint: NSLayoutConstraint!
var heightConstraint: NSLayoutConstraint!
var leadingConstraint: NSLayoutConstraint!
var trailingConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[pzc,s,b1,b2].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
[b1,b2].forEach {
$0.backgroundColor = .systemRed
}
pzc.backgroundColor = .systemGray
b1.frame = CGRect(x: view.center.x-115, y: view.center.y+200, width: 30, height: 30)
b2.frame = CGRect(x: view.center.x-115, y: view.center.y+250, width: 30, height: 30)
s.addTarget(self, action: #selector(moveRight), for: .touchUpInside)
b1.addTarget(self, action: #selector(mr1), for: .touchUpInside)
b2.addTarget(self, action: #selector(mr2), for: .touchUpInside)
NSLayoutConstraint.activate ([
b1.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant :37.5),
b1.topAnchor.constraint(equalTo: view.centerYAnchor, constant : 225),
b1.widthAnchor.constraint(equalToConstant: 75),
b1.heightAnchor.constraint(equalToConstant: 50),
b2.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant :130),
b2.topAnchor.constraint(equalTo: view.centerYAnchor, constant : 225),
b2.widthAnchor.constraint(equalToConstant: 75),
b2.heightAnchor.constraint(equalToConstant: 50),
])
s.minimumValue = 50
s.maximumValue = 200
s.setValue(jessicaAlba, animated: false)
view.addSubview(s)
s.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
s.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
s.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10).isActive = true
pzc.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pzc)
topConstraint = pzc.topAnchor.constraint(equalTo: view.topAnchor, constant: CGFloat(jessicaAlba))
topConstraint.isActive = true
heightConstraint = pzc.heightAnchor.constraint(equalTo: view.heightAnchor , multiplier: 0.5, constant: CGFloat(-jessicaAlba))
heightConstraint.isActive = true
leadingConstraint = pzc.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: CGFloat(jessicaAlba))
leadingConstraint.isActive = true
trailingConstraint = pzc.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: CGFloat(-jessicaAlba))
trailingConstraint.isActive = true
}
#objc func moveRight() {
if s.tag == 1 {
pzc.alpha = CGFloat(s.value)
}
if s.tag == 2 {
changeSize()
}
}
#objc func changeSize() {
UIView.animate(withDuration: 0.5, animations: {
self.jessicaAlba = self.s.value
self.topConstraint.constant = CGFloat(self.jessicaAlba)
self.heightConstraint.constant = CGFloat(-self.jessicaAlba)
self.leadingConstraint.constant = CGFloat(self.jessicaAlba)
self.trailingConstraint.constant = CGFloat(-self.jessicaAlba)
self.view.layoutIfNeeded()
}) { (finished) in
}
}
#objc func mr1() {
b1.backgroundColor = .brown
b2.backgroundColor = .systemPink
s.tag = 1
}
#objc func mr2() {
b2.backgroundColor = .brown
b1.backgroundColor = .systemPink
s.tag = 2
}
}
Here multiple target are added on your UISlider. Just modify your function.
#objc func moveRight() {
if s.tag == 1 {
let diff = s.maximumValue-s.minimumValue
pzc.alpha = CGFloat(s.value/diff)
}
if s.tag == 2 {
changeSize()
}
}
You can also modify your changeSize() function for animation.
#objc func changeSize() {
UIView.animate(withDuration: 0.5, animations: {
self.jessicaAlba = self.s.value
self.topConstraint.constant = CGFloat(self.jessicaAlba)
self.heightConstraint.constant = CGFloat(-self.jessicaAlba)
self.leadingConstraint.constant = CGFloat(self.jessicaAlba)
self.trailingConstraint.constant = CGFloat(-self.jessicaAlba)
self.view.layoutIfNeeded()
}) { (finished) in
}
}
I am attempting to display an imageView at the center in the X and Y axis onto a scrollView, fitted nicely within the width of the device when I run the app. I would also want to pan and zoom the imageView. I have tried almost all the solution that has been posted on SO and also followed this tutorial but somehow it just doesn't resolve my issue. The image is generated from a PDF.
I do my layout programmatically. My implementation so far:
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.backgroundColor = .lightGray
return sv
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFill
return iv
}()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.addSubview(imageView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 10),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 10),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -10),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -10),
])
imageView.image = obtainThumbnail()
}
fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
let widthScale = size.width / imageView.bounds.width
let heightScale = size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateMinZoomScaleForSize(view.bounds.size)
}
func obtainThumbnail() -> UIImage? {
guard let url = Bundle.main.url(forResource: "drawings", withExtension: "pdf") else {return nil}
guard
let data = try? Data(contentsOf: url),
let page = PDFDocument(data: data)?.page(at: 0) else {
return nil
}
let pageSize = page.bounds(for: .mediaBox)
return page.thumbnail(of: CGSize(width: pageSize.width, height: pageSize.height), for: .mediaBox)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
This is my desired results when I run the app.
And this is the results when I run the app now, overstretched.
Would anyone advice what am I missing?
I think that views are loaded lazily in UIViewControllers so making the calculation later in the UI lifecycle should solve the problem. Removing the call in viewWillLayoutSubviews and instead using:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateMinZoomScaleForSize(view.bounds.size)
}
seems to work.
To make it work well, there are two places to fix:
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.addSubview(imageView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
//First place.
// scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
scrollView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
imageView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 10),
imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 10),
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -10),
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -10),
])
imageView.backgroundColor = UIColor.red
imageView.image = UIImage.init(named: "temp2.png")
//second place.
imageView.sizeToFit()
Hope it solved your problem well.
If you need the view in the middle, may try the following method:
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
sv.backgroundColor = .lightGray
sv.contentMode = .center
return sv
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = true
iv.contentMode = .scaleAspectFill
return iv
}()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.addSubview(imageView)
NSLayoutConstraint.activate([
scrollView.centerYAnchor.constraint(equalTo: view.centerYAnchor ),
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor ),
scrollView.contentLayoutGuide.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
scrollView.contentLayoutGuide.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
])
imageView.backgroundColor = UIColor.red
imageView.image = UIImage.init(named: "temp2.png")
imageView.sizeToFit()
updateMinZoomScaleForSize(view.bounds.size)
}
var constantHeight : CGFloat{
return min(view.bounds.height, imageView.bounds.height * scrollView.zoomScale)
}
var constantWidth : CGFloat{
return min( view.bounds.width, imageView.bounds.width * scrollView.zoomScale)
}
lazy var constraintHeight: NSLayoutConstraint = {
return scrollView.contentLayoutGuide.heightAnchor.constraint(equalToConstant: constantHeight)
}()
lazy var constraintWeight: NSLayoutConstraint = {
return
scrollView.contentLayoutGuide.widthAnchor.constraint(equalToConstant: constantWidth)
}()
fileprivate func updateMinZoomScaleForSize(_ size: CGSize) {
let widthScale = size.width / imageView.bounds.width
let heightScale = size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
imageView.frame = CGRect.init(x: 0, y: 0, width: minScale * imageView.bounds.width, height: minScale*imageView.bounds.height)
scrollView.maximumZoomScale = 10.0
scrollView.zoomScale = 1.0
constraintHeight.isActive = true
constraintWeight.isActive = true
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat){
constraintHeight.constant = constantHeight
constraintWeight.constant = constantWidth
scrollView.clipsToBounds = true
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
scrollView.clipsToBounds = false
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}