I have three sections, the header and a container where a tab bar can facilitate transition of pages all within that container. I want to build a UI like the figure below.
My approach is to create two container views one for the header and the other for tabbar and selected pages. The tabbar and selected pages are UICollectionViewCell. So I will be putting UICollectionViewCell in a UIView container.
in my BusinessHomeViewController.swift file
class BusinessHomeViewController: UIViewController {
let businessPagesContainer: BusinessPages = {
let bp = BusinessPages()
return bp
}()
func setupbusinessPagesContainerView(){
businessPagesContainer.heightAnchor.constraint(equalToConstant: 80).isActive = true
businessPagesContainer.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
businessPagesContainer.topAnchor.constraint(equalTo: businessDescriptionView.bottomAnchor, constant: 90).isActive = true
view.addSubview(menuBar)
menuBar.topAnchor.constraint(equalTo: pageToggleContainer.topAnchor, constant: 2).isActive = true
menuBar.centerXAnchor.constraint(equalTo: pageToggleContainer.centerXAnchor).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(businessPagesContainer)
setupbusinessPagesContainerView()
}
}
In my BusinessPages.swift file
import UIKit
class BusinessPages: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When I run the app, it crashes and gave this error "terminating with uncaught exception of type NSException". Is my approach wrong and how can I fix this?
If you're running into this problem make sure that you go to Main.storyboard, RIGHT click on the yellow box icon (view controller) at the top of the phone outline and DELETE the outlet(s) with yellow flags.
Related
right now I have a collectionView in which I can add cells by tapping the "addCell".
My goal is that if the user taps the "addCell" a view should appear where the user can type in a title for the cell and select an image like so:
Any idea on how I could realize that?
class ContentCell: UICollectionViewCell {
let testImage: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
let testLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.text = "Test Label"
v.font = UIFont(name: "Avenir Next-Bold", size: 18)
v.textColor = .darkGray
v.textAlignment = .center
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(testLabel)
contentView.addSubview(testImage)
NSLayoutConstraint.activate([
testImage.topAnchor.constraint(equalTo: contentView.topAnchor),
testImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
testImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
testImage.heightAnchor.constraint(equalToConstant:150),
testLabel.topAnchor.constraint(equalTo: testImage.bottomAnchor,constant: 1),
testLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
testLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
testLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
so in didSelectItemAtIndexPath you present the view controller to ask for the image image and title, and then in that view controller you declare a delegate or completion handler that when the user presses "done" or "save" in the view controller where they selected the image and title, the delegate or completion handler is then called and then tells the original view controller with the cells to add the item you just create and reload the collectionView. this is pretty standard stuff and this same pattern you'll need to learn how to use since most apps use this this same pattern 100s of times per app. i can show you from Objective-C but won't waste my time showing this is Swift. lmk if you need more help. good luck
oh yes, and for future reference, it's best to stick with delegation since using completion handlers gets very tricky when you encounter cases with nested completions and still need to maintain a weakified then strongified reference to the controller. after two degress of weakifying and strogifying "self" the system will start to randomly deallocate your controllers. Stick with delegation.
I've created a subclass to manage my Theme but is not showing neither on device or simulator.
Here my Header.swift:
import Foundation
import UIKit
class Header: UILabel {
override var textColor: UIColor! {
// White Color
get { return ThemeManager.currentTheme.palette.primary }
set {}
}
override var font: UIFont! {
get { return ThemeManager.currentTheme.textStyle.headerText }
set {}
}
}
Here the implementation: (inside the viewcontroller)
var titleLabel: Header = Header()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Background Image over the view
setupBackground()
setupStartButton()
setupTitleLabel()
print(titleLabel.frame)
}
// MARK: - Header
private func setupTitleLabel() {
titleLabel.text = "0.0m"
// titleLabel.font = ThemeManager.currentTheme.textStyle.headerText
// titleLabel.textColor = ThemeManager.currentTheme.palette.primary
view.addSubview(titleLabel)
view.bringSubviewToFront(titleLabel)
setupTitleLabelAutolayout()
}
private func setupTitleLabelAutolayout() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
But if I use UILabel instead of Header it works perfectly as expected.
I've also tried to implement init?(coder aDecoder: NSCoder) and init(frame: CGRect) but nothing changed.
If I set a frame on init then shows the text, but not styled and ignoring my constraints.
Surely I'm missing something, but what?
To avoid usefulness answers, here some infos:
The UILabel textColor is white
The background is black and has an image over it.
I've tried to remove the image and all the stuff around except for the label and nothing changed.
That's a poor reason to use subclassing. It doesn't allow you to mix-and-match when appropriate.
Better would be to make an extension:
extension UILabel {
func withHeaderStyle() -> UILabel {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
return self
}
}
Then at point of use:
var titleLabel = UILabel().withHeaderStyle()
You can make several of these "withXStyle" methods and at the point of use you can chain them together. That's something you can't do with inheritance.
In general you should only use inherence when you want to change behavior. It's ill suited for changing data.
I've fixed that by editing the Header to this:
import Foundation
import UIKit
class Header: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupStyle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupStyle()
}
private func setupStyle() {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
}
}
Basically if I understood right, when I set the getter in the label it doesn't (if you think about, it's quite obvious) anything.
I still think that there are better solutions, but this works fine for me so, I'll keep that.
Now you may ask: "Why did you overwritten the getter instead of doing this?"
It's the right question, and the right answer is that I read it in a swift article on medium, so I tought it was right.
PS: I've also tried with didSet but it obviously loop through it self and crash.
the problem I am having is that I have reusable views / controls that contain text fields. These are xib files with a custom UI view class, such as the following:
import UIKit
#IBDesignable
public class CustomControl: UIControl {
#IBOutlet public weak var textField: UITextField?
public var contentView: UIView?
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViewFromNib()
}
override public init(frame: CGRect) {
super.init(frame: frame)
setupViewFromNib()
}
override public func awakeFromNib() {
super.awakeFromNib()
setupViewFromNib()
}
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setupViewFromNib()
contentView?.prepareForInterfaceBuilder()
}
func setupViewFromNib() {
guard let view = loadViewFromNib() else { return }
guard let textField = self.textField else { return }
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let selfType = type(of: self)
let nibName = String(describing: selfType)
return Bundle(for: selfType)
.loadNibNamed(nibName, owner: self, options: nil)?
.first as? UIView
}
}
This custom view is being loaded into the Storyboards where they are to be used using the Storyboard Interface Builder.
The problem is that XCTest does not seem to model the descendants of these views, so when I am trying to write a test that involves typing text into the text field that is part of the custom view, the test bombs out with the error:
Neither element nor any descendant has keyboard focus.
Currently a work around appears to be to tap the keys on the keyboard instead of using the typeText method. However this is much slower (long pauses between key presses) and much more cumbersome test code wise.
The desired code:
let app = XCUIApplication()
let view = app.otherElements["customView"]
let textField = view.textFields["textField"]
textField.tap()
textField.typeText("12345")
Using test recording we get something like:
let app = XCUIApplication()
let view = app.otherElements["customView"]
view.tap()
app.typeText("12345")
But running this test causes the aforementioned error.
The edited / working test becomes:
let app = XCUIApplication()
let view = app.otherElements["customView"]
// view appears as a leaf with no descendants
view.tap()
app.keys["1"].tap()
app.keys["2"].tap()
app.keys["3"].tap()
app.keys["4"].tap()
app.keys["5"].tap()
I’m also not convinced this workaround will remain feasible if the custom view were to contain multiple controls, say perhaps for a date of birth control where I want more granular control over which field within the custom control I am using.
Is there a solution that allows me to access the fields within a custom view and potentially use the typeText method?
The problem has been solved. As advised by Titouan de Bailleul, the problem was that accessibility for the custom view had been enabled effectively hiding its descendant text fields.
Added sample project to Github:
https://github.com/stuartwakefield/XibXCTestIssueSample
Thanks Titouan.
I am working on Apple's Food Tracker tutorial in Swift 3.2 and cannot get the ratings' backgrounds to display properly. Everything is fine to this point; no warnings or errors. The ratings' backgrounds (red squares) are embedded in a horizontal view stack. Five red squares display horizontally, which is correct, but four red squares display vertically also, which is incorrect. I've triple checked the code and all looks okay. I've also searched Stack Overflow but found nothing that helped. I've included the code for the RatingControl.swift file for reference. (I am unable to post an image b/c I don't have sufficient reputation points.) Is the problem possibly related to constraints? Any help would be appreciated. [ratings buttons][1]
//
// RatingControl.swift
// FoodTracker
//
// Created by Jane Appleseed on 10/17/16.
// Copyright © 2016 Apple Inc. All rights reserved.
//
import UIKit
#IBDesignable class ratingControl: UIStackView {
//MARK: Properties
private var ratingButtons = [UIButton]()
var rating = 0
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder){
super.init(coder: coder)
setupButtons()
}
//MARK: Button Action
func ratingButtonTapped(button: UIButton) {
print("Button pressed 👍")
}
//MARK: Private Methods
private func setupButtons() {
for _ in 0..<5 {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.red
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(ratingControl.ratingButtonTapped(button:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
// Add the new button to the rating button array
ratingButtons.append(button)
}
}
}
//![ratings buttons][1]
//[1]: https://i.stack.imgur.com/ofa78.jpg
I'm using Swift and Xcode 6.4 for what it's worth.
So I have a view controller that will be containing some multiple pairs of UILabels and UIImageViews. I wanted to put the UILabel-UIImageView pair into a custom UIView, so I could simply reuse the same structure repeatedly within the aforementioned view controller. (I'm aware this could be translated into a UITableView, but for the sake of ~learning~ please bear with me). This is turning out to be a more convoluted process than I imagined it would be, I'm having trouble figuring out the "right" way to make this all work in IB.
Currently I've been floundering around with a UIView subclass and corresponding XIB, overriding init(frame:) and init(coder), loading the view from the nib and adding it as a subview. This is what I've seen/read around the internet so far. (This is approximately it: http://iphonedev.tv/blog/2014/12/15/create-an-ibdesignable-uiview-subclass-with-code-from-an-xib-file-in-xcode-6).
This gave me the problem of causing an infinite loop between init(coder) and loading the nib from the bundle. Strangely none of these articles or previous answers on stack overflow mention this!
Ok so I put a check in init(coder) to see if the subview had already been added. That "solved" that, seemingly. However I started running into an issue with my custom view outlets being nil by the time I try to assign values to them.
I made a didSet and added a breakpoint to take a look...they are definitely being set at one point, but by the time I try to, say, modify the textColor of a label, that label is nil.
I'm kind of tearing my hair out here.
Reusable components seem like software design 101, but I feel like Apple is conspiring against me. Should I be looking to use container VCs here? Should I just be nesting views and having a stupidly huge amount of outlets in my main VC? Why is this so convoluted? Why do everyone's examples NOT work for me?
Desired result (pretend the whole thing is the VC, the boxes are the custom uiviews I want):
Thanks for reading.
Following is my custom UIView subclass. In my main storyboard, I have UIViews with the subclass set as their class.
class StageCardView: UIView {
#IBOutlet weak private var stageLabel: UILabel! {
didSet {
NSLog("I will murder you %#", stageLabel)
}
}
#IBOutlet weak private var stageImage: UIImageView!
var stageName : String? {
didSet {
self.stageLabel.text = stageName
}
}
var imageName : String? {
didSet {
self.stageImage.image = UIImage(named: imageName!)
}
}
var textColor : UIColor? {
didSet {
self.stageLabel.textColor = textColor
}
}
var splatColor : UIColor? {
didSet {
let splatImage = UIImage(named: "backsplat")?.tintedImageWithColor(splatColor!)
self.backgroundColor = UIColor(patternImage: splatImage!)
}
}
// MARK: init
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if self.subviews.count == 0 {
setup()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
if let view = NSBundle.mainBundle().loadNibNamed("StageCardView", owner: self, options: nil).first as? StageCardView {
view.frame = bounds
view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
addSubview(view)
}
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
}
EDIT: Here's what I've been able to get so far...
XIB:
Result:
Problem: When trying to access label or image outlets, they are nil. When checking at breakpoint of said access, the label and image subviews are there and the view hierarchy is as expected.
I'm OK with doing this all in code if thats what it takes, but I'm not huge into doing Autolayout in code so I'd rather not if there's a way to avoid it!
EDIT/QUESTION SHIFT:
I figured out how to make the outlets stop being nil.
Inspiration from this SO answer: Loaded nib but the view outlet was not set - new to InterfaceBuilder except instead of assigning the view outlet I assigned the individual component outlets.
Now this was at the point where I was just flinging shit at a wall and seeing if it'd stick. Does anyone know why I had to do this? What sort of dark magic is this?
General advice on view re-use
You're right, re-usable and composable elements is software 101. Interface Builder is not very good at it.
Specifically, xibs and storyboard are great ways to define views by re-using views that are defined in code. But they are not very good for defining views that you yourself wish to re-use within xibs and storyboards. (It can be done, but it is an advanced exercise.)
So, here's a rule of thumb. If you are defining a view that you want to re-use from code, then define it however you wish. But if you are defining a view that you want to be able to re-use possibly from within a storyboard, then define that view in code.
So in your case, if you're trying to define a custom view which you want to re-use from a storyboard, I'd do it in code. If you are dead set on defining your view via a xib, then I'd define a view in code and in its initializer have it initialize your xib-defined view and configure that as a subview.
Advice in this case
Here's roughly how you'd define your view in code:
class StageCardView: UIView {
var stageLabel = UILabel(frame:CGRectZero)
var stageImage = UIImageView(frame:CGRectZero)
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
required init(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)
setup()
}
private func setup() {
stageImage.image = UIImage(named:"backsplat")
self.addSubview(stageLabel)
self.addSubview(stageImage)
// configure the initial layout of your subviews here.
}
}
You can now instantiate this in code and or via a storyboard, although you won't get a live preview in Interface Builder as is.
And alternatively, here's roughly how you might define a re-usable view based fundamentally on a xib, by embedding the xib-defined view in a code-defined view:
class StageCardView: UIView {
var embeddedView:EmbeddedView!
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
required init(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)
setup()
}
private func setup() {
self.embeddedView = NSBundle.mainBundle().loadNibNamed("EmbeddedView",owner:self,options:nil).lastObject as! UIView
self.addSubview(self.embeddedView)
self.embeddedView.frame = self.bounds
self.embeddedView.autoresizingMask = .FlexibleHeight | .FlexibleWidth
}
}
Now you can use the code-defined view from storyboards or from code, and it will load its nib-defined subview (and there's still no live preview in IB).
I was able to work it around but the solution is a little bit tricky. It's up to debate if the gain is worth an effort but here is how I implemented it purely in interface builder
First I defined a custom UIView subclass named P2View
#IBDesignable class P2View: UIView
{
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var iconView: UIImageView!
#IBInspectable var title: String? {
didSet {
if titleLabel != nil {
titleLabel.text = title
}
}
}
#IBInspectable var image: UIImage? {
didSet {
if iconView != nil {
iconView.image = image
}
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
awakeFromNib()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override func awakeFromNib()
{
super.awakeFromNib()
let bundle = Bundle(for: type(of: self))
guard let view = bundle.loadNibNamed("P2View", owner: self, options: nil)?.first as? UIView else {
return
}
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
let bindings = ["view": view]
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"V:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"H:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
addConstraints(verticalConstraints)
addConstraints(horizontalConstraints)
}
titleLabel.text = title
iconView.image = image
}
This is how it looks like in interface builder
This is how I embedded this custom view in the example view controller defined on a storyboard. Properties of P2View are set in the attributes inspector.
There are 3 points worth mentioning
First:
Use the Bundle(for: type(of: self)) when loading the nib. This is because the interface builder renders the designables in the separate process which main bundle is not the same as your main bundle.
Second:
#IBInspectable var title: String? {
didSet {
if titleLabel != nil {
titleLabel.text = title
}
}
}
When combining IBInspectables with IBOutlets you have to remember that the didSet functions are called before awakeFromNib method. Because of that, the outlets are not initialized and your app will probably crash at this point. Unfortunatelly you cannot omit the didSet function because the interface builder won't render your custom view so we have to leave this dirty if here.
Third:
titleLabel.text = title
iconView.image = image
We have to somehow initialize our controls. We were not able to do it when didSet function was called so we have to use the value stored in the IBInspectable properties and initialize them at the end of the awakeFromNib method.
This is how you can implement a custom view on a Xib, embed it on a storyboard, configure it on a storyboard, have it rendered and have a non-crashing app. It requires a hack, but it's possible.