How to change collectionview cells color based on device theme (following my color scheme) - ios

Overview:
I'm building a keyboard Extension using collectionviews. I want the cells to change color based on the device theme (light/dark). At the moment, when I set the color scheme for my collectionview cells they don't work. I'm marking the problematic parts of my code with a "///" comment.
Resources:
I found this RayWenderlich project and I liked how they handled the color changing stuff so I copied it.
My code:
I have 3 classes:
KeyboardViewController
Custom View containing keyboard buttons
Custom collectionview cells
CollectionView cell
class KeyboardKeys: UICollectionViewCell {
var defaultColor = UIColor.white
var highlighColor = UIColor.lightGray.withAlphaComponent(0.6)
let label: UILabel = {
let iv = UILabel()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFit
iv.font = UIFont.systemFont(ofSize: 20)
iv.clipsToBounds = true
iv.numberOfLines = 1
iv.textAlignment = .center
return iv
}()
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
contentView.addSubview(label)
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
}
Custom View
class lettersKeyboard: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
var keyView: UICollectionView!
let letters = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
//If you find some errors it's because this is way different in my code. This is just a regulare collection view anyway
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
keyView = UICollectionView(frame: CGRect(x: 0.0, y: 0.0 , width: frame.width, height: 280), collectionViewLayout: layout)
keyView.setCollectionViewLayout(layout, animated: true)
keyView.isScrollEnabled = false
keyView.register(KeyboardKeys.self, forCellWithReuseIdentifier: "collectionCellId")
keyView.delegate = self
keyView.dataSource = self
addSubview(keyView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = keyView.dequeueReusableCell(withReuseIdentifier: "collectionCellId", for: indexPath) as! KeyboardKeys
cell.label.text = letters[indexPath.row]
return cell
}
///I guess something is wrong here
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
}
}
}
}
Color scheme struct
enum ColorScheme {
case dark
case light
}
struct CColors {
let keysDefaultColor: UIColor
let keysHighlightColor: UIColor
let buttonTextColor: UIColor
init(colorScheme: ColorScheme) {
switch colorScheme {
case .light:
keysDefaultColor = .systemRed
//UIColor.white
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.6)
buttonTextColor = .black
case .dark:
keysDefaultColor = .systemBlue
// UIColor.gray.withAlphaComponent(0.5)
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.5)
buttonTextColor = .white
}
}
}
KeyboardViewController
class KeyboardViewController: UIInputViewController {
var letters : lettersKeyboard = {
let m = lettersKeyboard(frame: .zero)
m.translatesAutoresizingMaskIntoConstraints = false
m.backgroundColor = .clear
return m
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(letters)
letters.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
letters.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
letters.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
letters.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
//The rest is the default inputvc stuff
///Or here
override func textDidChange(_ textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
let colorScheme: ColorScheme
let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.dark {
colorScheme = .dark
} else {
colorScheme = .light
}
letters.setColorScheme(colorScheme)
}
}
Question:
I don't know what I'm doing wrong since my code works with everything except for collectionview cells. I guess another way of doing this stuff exists. So how do I change my collectionView cells' color based on the device's theme following my color scheme?

You should really be reloading the collection view, rather than trying to find the subviews that are the keys, and updating those.
Pass in the colorScheme model to each cell and have the colors be set as a result of a reload.

A very kind guy helped me out and found this solution. The problem here is that I forgot the view's hierarchy.
CollectionView cell
override func layoutSubviews() {
super.layoutSubviews()
setupBackGround()
}
func setupBackGround(){
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
KeyboardViewController
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
func setToRootView(view: UIView) {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
cell.setBackground()
return
}
guard view.subviews.count > 0 else {
return
}
view.subviews.forEach(setToRootView(view:))
}
setToRootView(view: self)
}

Related

collection view presentation of graph: "no chart data available" for Charts cocoapods

I'm coding a GraphViewController class that houses an array of graphs (of type LineChartView). However, when I attempt to display these graphs in the format of cells of a collection view (using the called class GraphCell), the LineChartView objects don't seem to load any data, even though these functions are called inside the GraphViewController class. Here are the relevant bits of my code so far:
class GraphViewController: UIViewController {
//lazy var only calculated when called
lazy var lineChartView: LineChartView = {
let chartView = LineChartView()
chartView.backgroundColor = .systemBlue
chartView.rightAxis.enabled = false //right axis contributes nothing
let yAxis = chartView.leftAxis
yAxis.labelFont = .boldSystemFont(ofSize: 12)
yAxis.setLabelCount(6, force: false)
yAxis.labelTextColor = .white
yAxis.axisLineColor = .white
yAxis.labelPosition = .outsideChart
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.labelFont = .boldSystemFont(ofSize: 12)
xAxis.setLabelCount(6, force: false)
xAxis.labelTextColor = .white
xAxis.axisLineColor = .systemBlue
chartView.animate(xAxisDuration: 1)
return chartView
}()
var graphColl: UICollectionView!
var graphs: [LineChartView] = []
var graphReuseID = "graph"
var homeViewController: ViewController = ViewController()
let dataPts = 50
var yValues: [ChartDataEntry] = []
var allWordsNoPins: [Word] = []
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setUpViews();
setUpConstraints()
}
func setUpViews(){
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = padding/3.2
layout.scrollDirection = .vertical
graphColl = UICollectionView(frame: .zero, collectionViewLayout: layout)
graphColl.translatesAutoresizingMaskIntoConstraints = false
graphColl.dataSource = self
graphColl.delegate = self
//must register GraphCell class before calling dequeueReusableCell
graphColl.register(GraphCell.self, forCellWithReuseIdentifier: graphReuseID)
graphColl.backgroundColor = .white
view.addSubview(graphColl)
print("stuff assigned")
assignData()
setData()
graphs.append(lineChartView)
}
One can assume setUpConstraints() is working correctly, as the graph collection does show up. Here are all the functions that have to deal with the collection view I'm using:
//INSIDE GraphViewController
extension GraphViewController: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
//collectionView.frame.height -
let size = 25*padding
let sizeWidth = collectionView.frame.width - padding/3
return CGSize(width: sizeWidth, height: size)
}
}
extension GraphViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return graphs.count //total number of entries
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let graphColl = collectionView.dequeueReusableCell(withReuseIdentifier: graphReuseID, for: indexPath) as! GraphCell
graphColl.configure(graph: graphs[indexPath.item])
return graphColl
}
}
and:
//configure function INSIDE GraphCell
func configure(graph: LineChartView){
chartView = graph
}
Here are the assignData() and setData() functions:
func setData(){
let set1 = LineChartDataSet(entries: yValues, label: "Word Frequency")
let data = LineChartData(dataSet: set1)
set1.drawCirclesEnabled = true
set1.circleRadius = 3
set1.mode = .cubicBezier //smoothes out curve
set1.setColor(.white)
set1.lineWidth = 3
set1.drawHorizontalHighlightIndicatorEnabled = false //ugly yellow line
data.setDrawValues(false)
lineChartView.data = data
}
func assignData(){
setUpTempWords()
let dataValues = allWordsNoPins
print(allWordsNoPins.count)
for i in 0...dataPts-1{
yValues.append(ChartDataEntry(x: Double(i), y: Double(dataValues[i].count)))
}
}
One can also assume the setUpTempWords() function is working correctly, because of this screenshot below:
Here, I have plotted the lineChartView object of type LineChartView directly on top of the GraphColl UICollectionView variable inside my GraphViewController class. The data is displayed. However, when I try to plot the same graph in my GraphCell class, I get
"No chart data available." I have traced the calls in my GraphViewController class, and based on the way the viewDidLoad() function is set up, I can conclude that the lineChartView setup methods (assignData(), etc) are being called. For reference, here is my GraphCell class code:
import UIKit
import Charts
class GraphCell: UICollectionViewCell {
var chartView = LineChartView()
var graphCellBox: UIView!
override init(frame: CGRect){
super.init(frame: frame)
contentView.backgroundColor = .blue
chartView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(chartView)
graphCellBox = UIView()
graphCellBox.translatesAutoresizingMaskIntoConstraints = false
//graphCellBox.backgroundColor = cellorange
graphCellBox.layer.cornerRadius = 15.0
contentView.addSubview(graphCellBox)
setUpConstraints()
}
func setUpConstraints(){
NSLayoutConstraint.activate([
graphCellBox.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
graphCellBox.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
graphCellBox.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
graphCellBox.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
chartView.topAnchor.constraint(equalTo: graphCellBox.topAnchor),
chartView.bottomAnchor.constraint(equalTo: graphCellBox.bottomAnchor),
chartView.leadingAnchor.constraint(equalTo: graphCellBox.leadingAnchor),
chartView.trailingAnchor.constraint(equalTo: graphCellBox.trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(graph: LineChartView){
chartView = graph
}
}
As a side note, changing the lineChartView to a non-lazy (normal) variable does not fix this problem. I suspected the lazy declaration was the problem, since the graph would only be initialize if called, but this was not the case. Thank you for reading this post, and I'd greatly appreciate any direction or guidance!
Tough to test this without a reproducible example, but...
Assigning chartView = graph looks problematic.
Try using your graphCellBoxgraphCellBox as a "container" for the LineChartView you're passing in with configure(...):
class GraphCell: UICollectionViewCell {
var graphCellBox: UIView!
override init(frame: CGRect){
super.init(frame: frame)
contentView.backgroundColor = .blue
graphCellBox = UIView()
graphCellBox.translatesAutoresizingMaskIntoConstraints = false
//graphCellBox.backgroundColor = cellorange
graphCellBox.layer.cornerRadius = 15.0
contentView.addSubview(graphCellBox)
setUpConstraints()
}
func setUpConstraints(){
NSLayoutConstraint.activate([
graphCellBox.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
graphCellBox.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
graphCellBox.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
graphCellBox.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(graph: LineChartView){
graph.translatesAutoresizingMaskIntoConstraints = false
graphCellBox.addSubview(graph)
NSLayoutConstraint.activate([
graph.topAnchor.constraint(equalTo: graphCellBox.topAnchor),
graph.bottomAnchor.constraint(equalTo: graphCellBox.bottomAnchor),
graph.leadingAnchor.constraint(equalTo: graphCellBox.leadingAnchor),
graph.trailingAnchor.constraint(equalTo: graphCellBox.trailingAnchor),
])
}
}

Stackview inside collectionview or uitableview

I am trying to achieve a layout of a text followed by an image (image height calculated based on aspect ratio) then followed by text and so on. The issue is that the stackview that I am adding the views into randomly squash the views sometimes the imageviews disappear some time the text, it doesn't have a consistent behaviour.
i tried it on both uitableview and uicolletion view and the result is the same. is the combination of the mentioned views considered as a best practice for such usecase or not ? and if not what might be the best practice for such thing ?
class MyStackyView: UIStackView {
// Main variables
weak var videoPlayerDelegate: AVPlayerViewDelegate?
private var avVideoPlayersVC: [AVPlayerViewController] = []
var content: [Content]! {
didSet {
contentCombined = Utility.shared.combineToNew(contents: content)
}
}
private var contentCombined: [Content] = [] {
didSet {
populatePostContent()
}
}
var contentViews: [UIView] = [] // Holds the views created
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("DiaryPostView:: Deinitalized")
}
private func configureView() {
axis = .vertical
distribution = .fill
alignment = .fill
spacing = 0
}
}
// Extension to populate post content
extension MyStackyView {
private func populatePostContent() {
for content in contentCombined {
if content.isMedia {
addMedia(content)
} else {
addText(content.text)
}
}
}
}
// Extension to add the required views
extension MyStackyView {
private func addText(_ text: String?, place: MediaPlace = .center) {
let textView = generateDefaultTextView()
//let parsedText = HTMLParser.shared.parseHTMLToAttributed(string: text ?? "") // fix font issue
switch place {
case .center:
append(textView)
contentViews.append(textView)
}
textView.text = text
// لما استخدم ال parsedtext مرة النص بطلع مع الfont و مرة لا
}
private func addMedia(_ content: Content) {
let avPlayerVC = getAVPlayerViewController()
let mediaView = generateDefaultMediaView()
switch content.getRawPlace() {
case .center:
append(mediaView)
contentViews.append(mediaView)
addText(content.text)
NetworkManager().downloadMedia(content.img!, into: mediaView, avPlayerViewController: avPlayerVC) {
}
}
}
}
extension MyStackyView {
private func generateDefaultTextView() -> UILabel {
let textView = UILabel()
textView.backgroundColor = .clear
textView.numberOfLines = 0
textView.font = UIFont.customFont(.openSans, .regular, .title1, 17)
return textView
}
private func generateDefaultHorizontalStack() -> UIStackView {
let horizontalStack = UIStackView()
horizontalStack.axis = .horizontal
horizontalStack.distribution = .fill
horizontalStack.alignment = .fill
return horizontalStack
}
private func generateDefaultMediaView() -> MediaSliderView {
let mediaSliderView = MediaSliderView()
return mediaSliderView
}
private func getAVPlayerViewController() -> AVPlayerViewController? {
videoPlayerDelegate?.getAVPlayerVC?()
}
func deallocateAVPlayers() {
for player in avVideoPlayersVC {
player.removeFromParent()
}
avVideoPlayersVC.removeAll()
}
}
i initalize a variable of the class in my uitableviewcell and then add these constraints
contentView.addSubview(MyStackyView)
MyStackyView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true
MyStackyView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true
MyStackyView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true
MyStackyView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true
please if possible, i need some guidance about this issue.
thank you, appreciate the help
Here is a (fairly) basic example.
We'll use a data structure like this:
struct VarDevStruct {
var first: String = ""
var second: String = ""
var imageName: String = ""
}
The cell class has a vertical stack view containing:
multiline label
horizontal stack view
with an 80x80 image view and a label
multiline label
If any of the elements in the data struct are empty strings, we'll set the corresponding element in the cell to hidden.
First, the result:
after scrolling down to a few rows with different data:
and rotated:
Here's the complete code... plenty of comments in it, so it should be clear what the code is doing.
Data Structure
struct VarDevStruct {
var first: String = ""
var second: String = ""
var imageName: String = ""
}
Cell class
class VarDevCell: UITableViewCell {
let firstLabel = UILabel()
let secondLabel = UILabel()
let imgView = UIImageView()
let imgNameLabel = UILabel()
let vStack = UIStackView()
let hStack = UIStackView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// stack view properties
vStack.axis = .vertical
vStack.alignment = .fill
vStack.distribution = .fill
vStack.spacing = 8
vStack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(vStack)
// let's use the default cell margins
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides
vStack.topAnchor.constraint(equalTo: g.topAnchor),
vStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
vStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// subview properties
// background colors to make it easy to see the frames
firstLabel.backgroundColor = .yellow
secondLabel.backgroundColor = .green
imgView.backgroundColor = .red
imgNameLabel.backgroundColor = .cyan
// multi-line labels
firstLabel.numberOfLines = 0
secondLabel.numberOfLines = 0
imgNameLabel.textAlignment = .center
// image view defaults to scaleToFill
// let's set it to scaleAspectFit
imgView.contentMode = .scaleAspectFit
// horizontal stack view
hStack.axis = .horizontal
hStack.alignment = .center
hStack.distribution = .fill
hStack.spacing = 8
// add subviews to horizontal stack view
hStack.addArrangedSubview(imgView)
hStack.addArrangedSubview(imgNameLabel)
// let's fill the vertical stack view with
// label
// hStack with 80x80 imageview and label with image name
// label
vStack.addArrangedSubview(firstLabel)
vStack.addArrangedSubview(hStack)
vStack.addArrangedSubview(secondLabel)
// set image view width and height
imgView.widthAnchor.constraint(equalToConstant: 80.0).isActive = true
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0).isActive = true
}
func fillData(_ vdStruct: VarDevStruct) -> Void {
firstLabel.text = vdStruct.first
secondLabel.text = vdStruct.second
imgNameLabel.text = vdStruct.imageName
// does our data have an image name?
if !vdStruct.imageName.isEmpty {
if #available(iOS 13.0, *) {
if let img = UIImage(systemName: vdStruct.imageName) {
imgView.image = img
}
} else {
// Fallback on earlier versions
if let img = UIImage(named: vdStruct.imageName) {
imgView.image = img
}
}
}
// hide elements that we don't need in this cell
firstLabel.isHidden = vdStruct.first.isEmpty
secondLabel.isHidden = vdStruct.second.isEmpty
hStack.isHidden = vdStruct.imageName.isEmpty
}
}
Controller class
class VarDevTableViewController: UITableViewController {
var myData: [VarDevStruct] = []
override func viewDidLoad() {
super.viewDidLoad()
// register cell class for reuse
tableView.register(VarDevCell.self, forCellReuseIdentifier: "cell")
// generate some sample data
myData = makeSampleData()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// make sure table re-calculates row heights
UIView.setAnimationsEnabled(false)
self.tableView.performBatchUpdates(nil, completion: nil)
UIView.setAnimationsEnabled(true)
})
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! VarDevCell
cell.fillData(myData[indexPath.row])
return cell
}
func makeSampleData() -> [VarDevStruct] {
var a: [VarDevStruct] = []
// 15 sample data elements
for i in 1...15 {
let d = VarDevStruct(first: "This is the text for the first label in row: \(i).",
second: "This will be a longer string to be used as the text for the second label in row \(i) (long enough to make sure we're getting some word wrapping).",
imageName: "\(i).square.fill")
a.append(d)
}
// change some of the sample data for variations
// (arrays are zero-based)
// fifth row: no first label
a[4].first = ""
a[4].second = "This row has no First label text."
// sixth row: no image
a[5].first = "This row has no image."
a[5].imageName = ""
// seventh row: no second label
a[6].first = "This row has no second label."
a[6].second = ""
// eigth row: no image or second label
a[7].first = "This row has no image, and has no second label. The next row (9) has image only."
a[7].imageName = ""
a[7].second = ""
// ninth row: image only
a[8].first = ""
a[8].second = ""
// tenth row: first label with mutliple lines
a[9].first = "One\nTwo\nThree\nFour"
a[9].second = "This row has embedded newline chars in the text of the first label."
return a
}
}

How to add constraints to a collection view cell once the cell is selected?

I am trying to create a feature programmatically so that when a user selects a cell in the collection view the app keeps a count of the image selected and adds it as an overlay. I am also wanting to add the video duration to the bottom of the image if the selection is a video. I know my problem is in my constraints. You can see in the image example below that I am trying to add the count to the top left of the collection view cell, but also when the user deselects a cell the count adjusts so for example if the number 2 in the image below was deselected the number 3 would become 2. For the most part I think I have the code working but I cannot get the constraints to work. With the current configuration I am getting an error (see below) but I do not even know where to begin with this problem.
"Unable to activate constraint with anchors because they have
no common ancestor. Does the constraint or its anchors reference
items in different view hierarchies? That's illegal."
CollectionView:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.commonInit()
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
//Not sure what to put here
}
}
Overlay
class CustomAssetCellOverlay: UIView {
let countSize = CGSize(width: 40, height: 40)
lazy var circleView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = self.countSize.width / 2
view.alpha = 0.4
return view
}()
let countLabel: UILabel = {
let label = UILabel()
let font = UIFont.preferredFont(forTextStyle: .headline)
label.font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.bold)
label.textAlignment = .center
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
return label
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
addSubview(circleView)
addSubview(countLabel)
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor).isActive = true
countLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor).isActive = true
}
}
Collection View Cell
var img = UIImageView()
var overlayView = UIView()
var asset: PHAsset? {
didSet {}
}
var isVideo: Bool = false {
didSet {
durationLabel.isHidden = !isVideo
}
}
override var isSelected: Bool {
didSet { overlay.isHidden = !isSelected }
}
var imageView: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.backgroundColor = UIColor.gray
return view
}()
var count: Int = 0 {
didSet { overlay.countLabel.text = "\(count)" }
}
var duration: TimeInterval = 0 {
didSet {
let hour = Int(duration / 3600)
let min = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let sec = Int(duration.truncatingRemainder(dividingBy: 60))
var durationString = hour > 0 ? "\(hour)" : ""
durationString.append(min > 0 ? "\(min):" : ":")
durationString.append(String(format: "%02d", sec))
durationLabel.text = durationString
}
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.isHidden = true
return view
}()
let durationLabel: UILabel = {
let label = UILabel()
label.preferredMaxLayoutWidth = 80
label.backgroundColor = .gray
label.textColor = .white
label.textAlignment = .right
label.font = UIFont.boldSystemFont(ofSize: 20)
return label
}()
func commonInit() {
addSubview(imageView)
imageView.addSubview(overlay)
imageView.addSubview(durationLabel)
imageView.translatesAutoresizingMaskIntoConstraints = false
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
overlay.translatesAutoresizingMaskIntoConstraints = false
overlayView.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
NSLayoutConstraint.activate([
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leftAnchor.constraint(equalTo: imageView.leftAnchor),
overlay.rightAnchor.constraint(equalTo: imageView.rightAnchor),
overlayView.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
overlayView.centerYAnchor.constraint(equalTo: overlay.centerYAnchor),
overlayView.widthAnchor.constraint(equalToConstant: 80.0),
overlayView.heightAnchor.constraint(equalToConstant: 80.0),
]
)
}
//Some other stuff

UIScrollView not showing up in the view

I am implementing a UIScrollView in a CollectionViewCell. I have a custom view which the scroll view should display, hence I am performing the following program in the CollectionViewCell. I have created everything programmatically and below is my code :
struct ShotsCollections {
let title: String?
}
class ShotsMainView: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
containerScrollView.contentSize.width = frame.width * CGFloat(shotsData.count)
shotsData = [ShotsCollections.init(title: "squad"), ShotsCollections.init(title: "genral")]
var i = 0
for data in shotsData {
let customview = ShotsMediaView(frame: CGRect(x: containerScrollView.frame.width * CGFloat(i), y: 0, width: containerScrollView.frame.width, height: containerScrollView.frame.height))
containerScrollView.addSubview(customview)
i += 1
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var shotsData = [ShotsCollections]()
var containerScrollView: UIScrollView = {
let instance = UIScrollView()
instance.isScrollEnabled = true
instance.bounces = true
instance.backgroundColor = blueColor
return instance
}()
private func setupViews() { //These are constraints by using TinyConstraints
addSubview(containerScrollView)
containerScrollView.topToSuperview()
containerScrollView.bottomToSuperview()
containerScrollView.rightToSuperview()
containerScrollView.leftToSuperview()
}
}
Now the issue is, while the scrollview is displayed, the content in it is not. I on printing the contentSize and frame of the scrollview, it displays 0. But if I check the Debug View Hierarchy, scrollview containes 2 views with specific frames.
I am not sure whats going wrongs. Any help is appreciated.
When you are adding customView in your containerScrollView, you are not setting up the constraints between customView and containerScrollView.
Add those constraints and you will be able to see your customViews given that your customView has some height. Also, when you add more view, you would need to remove the bottom constraint of the last added view and create a bottom constraint to the containerScrollView with the latest added view.
I created a sample app for your use case. I am pasting the code and the resultant screen shot below. Hope this is the functionality you are looking for. I suggest you paste this in a new project and tweak the code until you are satisfied. I have added comments to make it clear.
ViewController
import UIKit
class ViewController: UIViewController {
// Initialize dummy data array with numbers 0 to 9
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
// Add collection view programmatically
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ShotsMainView.self, forCellWithReuseIdentifier: ShotsMainView.identifier)
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: collectionView.topAnchor),
self.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
self.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor.white
self.view.addSubview(collectionView)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.white
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShotsMainView.identifier, for: indexPath) as! ShotsMainView
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell dimensions are set from here
return CGSize(width: collectionView.frame.size.width, height: 100.0)
}
}
ShotsMainView
This is the collection view cell
import UIKit
class ShotsMainView: UICollectionViewCell {
static var identifier = "Cell"
weak var textLabel: UILabel!
override init(frame: CGRect) {
// Initialize with zero frame
super.init(frame: frame)
// Add the scrollview and the corresponding constraints
let containerScrollView = UIScrollView(frame: .zero)
containerScrollView.isScrollEnabled = true
containerScrollView.bounces = true
containerScrollView.backgroundColor = UIColor.blue
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerScrollView)
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
self.bottomAnchor.constraint(equalTo: containerScrollView.bottomAnchor),
self.leadingAnchor.constraint(equalTo: containerScrollView.leadingAnchor),
self.trailingAnchor.constraint(equalTo: containerScrollView.trailingAnchor)
])
// Add the stack view that will hold the individual items that
// in each row that need to be scrolled horrizontally
let stackView = UIStackView(frame: .zero)
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
containerScrollView.addSubview(stackView)
stackView.backgroundColor = UIColor.magenta
NSLayoutConstraint.activate([
containerScrollView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
containerScrollView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
containerScrollView.topAnchor.constraint(equalTo: stackView.topAnchor),
containerScrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
// Add individual items (Labels in this case).
for i in 0..<10 {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.text = "\(i)"
label.font = UIFont(name: "System", size: 20.0)
label.textColor = UIColor.white
label.backgroundColor = UIColor.purple
label.layer.masksToBounds = false
label.layer.borderColor = UIColor.white.cgColor
label.layer.borderWidth = 1.0
label.textAlignment = .center
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1.0, constant: 0.0),
label.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.2, constant: 0.0)
])
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Screenshot

state restoration working but then nullified in viewDidLoad

Note code has been updated to incorporate the fixes detailed in the comments, but here is the original question text:
State restoration works on the code-based ViewController below, but then it is "undone" by a second call to viewDidLoad. My question is: how do I avoid that?
With a breakpoint at decodeRestorableState I can see that it does in fact restore the 2 parameters selectedGroup and selectedType but then it goes through viewDidLoad again and those parameters are reset to nil so the restoration is of no effect. There's no storyboard: if you associated this class with an empty ViewController it will work (I double checked this -- there are some button assets too, but they aren't needed for function). I've also included at the bottom the AppDelegate methods needed to enable state restoration.
import UIKit
class CodeStackVC2: UIViewController, FoodCellDel {
let fruit = ["Apple", "Orange", "Plum", "Qiwi", "Banana"]
let veg = ["Lettuce", "Carrot", "Celery", "Onion", "Brocolli"]
let meat = ["Beef", "Chicken", "Ham", "Lamb"]
let bread = ["Wheat", "Muffin", "Rye", "Pita"]
var foods = [[String]]()
let group = ["Fruit","Vegetable","Meat","Bread"]
var sView = UIStackView()
let cellId = "cellId"
var selectedGroup : Int?
var selectedType : Int?
override func viewDidLoad() {
super.viewDidLoad()
restorationIdentifier = "CodeStackVC2"
foods = [fruit, veg, meat, bread]
setupViews()
displaySelections()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let index = selectedGroup, let type = selectedType else { return }
pageControl.currentPage = index
let indexPath = IndexPath(item: index, section: 0)
cView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition(), animated: true)
cView.reloadItems(at: [indexPath])
guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else { return }
cell.pickerView.selectRow(type, inComponent: 0, animated: true)
}
//State restoration encodes parameters in this func
override func encodeRestorableState(with coder: NSCoder) {
if let theGroup = selectedGroup,
let theType = selectedType {
coder.encode(theGroup, forKey: "theGroup")
coder.encode(theType, forKey: "theType")
}
super.encodeRestorableState(with: coder)
}
override func decodeRestorableState(with coder: NSCoder) {
selectedGroup = coder.decodeInteger(forKey: "theGroup")
selectedType = coder.decodeInteger(forKey: "theType")
super.decodeRestorableState(with: coder)
}
override func applicationFinishedRestoringState() {
//displaySelections()
}
//MARK: Views
lazy var cView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
layout.itemSize = CGSize(width: self.view.frame.width, height: 120)
let cRect = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 120)
let cv = UICollectionView(frame: cRect, collectionViewLayout: layout)
cv.backgroundColor = UIColor.lightGray
cv.isPagingEnabled = true
cv.dataSource = self
cv.delegate = self
cv.isUserInteractionEnabled = true
return cv
}()
lazy var pageControl: UIPageControl = {
let pageC = UIPageControl()
pageC.numberOfPages = self.foods.count
pageC.pageIndicatorTintColor = UIColor.darkGray
pageC.currentPageIndicatorTintColor = UIColor.white
pageC.backgroundColor = .black
pageC.addTarget(self, action: #selector(changePage(sender:)), for: UIControlEvents.valueChanged)
return pageC
}()
var textView: UITextView = {
let tView = UITextView()
tView.font = UIFont.systemFont(ofSize: 40)
tView.textColor = .white
tView.backgroundColor = UIColor.lightGray
return tView
}()
func makeButton(_ tag:Int) -> UIButton{
let newButton = UIButton(type: .system)
let img = UIImage(named: group[tag])?.withRenderingMode(.alwaysTemplate)
newButton.setImage(img, for: .normal)
newButton.tag = tag // used in handleButton()
newButton.contentMode = .scaleAspectFit
newButton.addTarget(self, action: #selector(handleButton(sender:)), for: .touchUpInside)
newButton.isUserInteractionEnabled = true
newButton.backgroundColor = .clear
return newButton
}
//Make a 4-item vertical stackView containing
//cView,pageView,subStackof 4-item horiz buttons, textView
func setupViews(){
view.backgroundColor = .lightGray
cView.register(FoodCell.self, forCellWithReuseIdentifier: cellId)
//generate an array of buttons
var buttons = [UIButton]()
for i in 0...foods.count-1 {
buttons += [makeButton(i)]
}
let subStackView = UIStackView(arrangedSubviews: buttons)
subStackView.axis = .horizontal
subStackView.distribution = .fillEqually
subStackView.alignment = .center
subStackView.spacing = 20
//set up the stackView
let stackView = UIStackView(arrangedSubviews: [cView,pageControl,subStackView,textView])
stackView.axis = .vertical
stackView.distribution = .fill
stackView.alignment = .fill
stackView.spacing = 5
//Add the stackView using AutoLayout
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
cView.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
cView.heightAnchor.constraint(equalTo: textView.heightAnchor, multiplier: 0.5).isActive = true
}
// selected item returned from pickerView
func pickerSelection(_ foodType: Int) {
selectedType = foodType
displaySelections()
}
func displaySelections() {
if let theGroup = selectedGroup,
let theType = selectedType {
textView.text = "\n \n Group: \(group[theGroup]) \n \n FoodType: \(foods[theGroup][theType])"
}
}
// 3 User Actions: Button, Page, Scroll
func handleButton(sender: UIButton) {
pageControl.currentPage = sender.tag
let x = CGFloat(sender.tag) * cView.frame.size.width
cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
}
func changePage(sender: AnyObject) -> () {
let x = CGFloat(pageControl.currentPage) * cView.frame.size.width
cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let index = Int(cView.contentOffset.x / view.bounds.width)
pageControl.currentPage = Int(index) //change PageControl indicator
selectedGroup = Int(index)
let indexPath = IndexPath(item: index, section: 0)
guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else { return }
selectedType = cell.pickerView.selectedRow(inComponent: 0)
displaySelections()
}
//this causes cView to be recalculated when device rotates
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
cView.collectionViewLayout.invalidateLayout()
}
}
//MARK: cView extension
extension CodeStackVC2: UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return foods.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FoodCell
cell.foodType = foods[indexPath.item]
cell.delegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: textView.frame.height * 0.4)
}
}
// *********************
protocol FoodCellDel {
func pickerSelection(_ food:Int)
}
class FoodCell:UICollectionViewCell, UIPickerViewDelegate, UIPickerViewDataSource {
var delegate: FoodCellDel?
var foodType: [String]? {
didSet {
pickerView.reloadComponent(0)
//pickerView.selectRow(0, inComponent: 0, animated: true)
}
}
lazy var pickerView: UIPickerView = {
let pView = UIPickerView()
pView.frame = CGRect(x:0,y:0,width:Int(pView.bounds.width), height:Int(pView.bounds.height))
pView.delegate = self
pView.dataSource = self
pView.backgroundColor = .lightGray
return pView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
backgroundColor = .clear
addSubview(pickerView)
addConstraintsWithFormat("H:|[v0]|", views: pickerView)
addConstraintsWithFormat("V:|[v0]|", views: pickerView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if let count = foodType?.count {
return count
} else {
return 0
}
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let pickerLabel = UILabel()
pickerLabel.font = UIFont.systemFont(ofSize: 15)
pickerLabel.textAlignment = .center
pickerLabel.adjustsFontSizeToFitWidth = true
if let foodItem = foodType?[row] {
pickerLabel.text = foodItem
pickerLabel.textColor = .white
return pickerLabel
} else {
print("chap = nil in viewForRow")
return UIView()
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if let actualDelegate = delegate {
actualDelegate.pickerSelection(row)
}
}
}
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
Here are the functions in AppDelegate:
//====if set true, these 2 funcs enable state restoration
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
//replace the storyboard by making our own window
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
//this defines the entry point for our app
window?.rootViewController = CodeStackVC2()
return true
}
If viewDidLoad is being called twice it will because your view controller is being created twice.
You do not say how you are creating the view controller but I suspect your problem is that the view controller is being created first by a storyboard or in the app delegate and then a second time because you have set a restoration class.
You only need to set a restoration class if your view controller is not being created by the normal app load sequence (a restoration identifier is enough otherwise). Try removing the line in viewDidLoad where you set a restoration class and I think you will see viewDidLoad is called once followed by decodeRestorableState.
Update: Confirmed you are creating the view controller in the app delegate so you do not need to use a restoration class. That fixes the problem with viewDidLoad being called twice.
You want to do the initial root view controller creation in willFinishLaunchingWithOptions in the app delegate as that is called before state restoration takes place.
The final issue once you have the selectedGroup and selectedType values restored is to update the UI elements (page control, collection view), etc to use the restored values
In my six years of iOS programming, I don't remember ever seeing iOS calling viewDidLoad() twice on the same view controller. So it is most likely that you are instantiating CodeStackVC2 twice :)
As far as I can tell, you are creating the view hierarchy programmatically in didFinishLaunchingWithOptions. However, state restoration is invoked before this delegate method is called. So, iOS asks the view controller's restoration class for a new view controller instance, and after that your code setting up the base hierarchy is executed, creating a fresh view controller.
Try moving your code from didFinishLaunchingWithOptions to willFinishLaunchingWithOptions: (which is called before any state restoration). Then, since the view controller that iOS is trying to restore already exists, it won't call that method with the long name from the UIViewControllerRestoration protocol, and instead call decodeRestorableState(with coder:) on that view controller.
If you need a more in-depth explanation, try useyourloaf or of course the Apple docs - I have found both to be very useful in understanding the concepts behind Apple's implementation. Although I must admit, I took me several reads before I understood it myself.

Resources