I have a UITableViewController with a list of custom UITableViewCells. The cell has text, which is sometimes long. I am truncating that at 2 lines in the table.
When a user touches the cell, I want to create a UIView as a popup with the exact same information as the UITableViewCell, with the addition of a header above the UITableViewCell content. So, essentially, I want something like the following:
I have built VersePopupView just as indicated in the picture, where the text "Custom Header" is actually a UILabel. I have instantiated the view from the XIB file and successfully set the UILabel text, but the custom UITableViewCell doesn't show, even though I have assigned that value as well. My IBOutlets are all hooked up to IB.
Here is my instantiation:
let popupView: VersePopupView = VersePopupView.instanceFromNib()
popupView.frame = CGRect(x: 10, y: 100, width: 300, height: 100)
popupView.assignVerseTableViewCell(cell: cell)
window.addSubview(popupView)
window.makeKeyAndVisible()
And here is my VersePopupView class:
class VersePopupView: UIView {
#IBOutlet weak var cell: VerseTableViewCell!
#IBOutlet weak var title: UILabel!
#IBOutlet weak var headerView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
class func instanceFromNib() -> VersePopupView {
return UINib(nibName: "VersePopupView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! VersePopupView
}
func assignVerseTableViewCell(cell: VerseTableViewCell)
{
// Assign it
self.cell = cell
// Get the verse details
if let verseDetails = cell.verse_details {
self.title.text = verseDetails.verseReference()
// Adjust the frame
// Adjust the text to be attributed text
// Set the title to the verse reference
// Anything else?
}
}
}
What am I doing wrong such that the UITableViewCell isn't showing up?
Is there a better pattern for this rather than embedding the UITableViewCell?
I don't want to duplicate code (which I have done in the past), because I want the user to interact with the popup just like they would in the table cell.
I'm not sure how to convince you of how easy this is to do without trying to misuse a table view cell, so here's a screen shot:
You just have to believe me when I say that that's a three-row table followed by a totally independent ordinary view plucked from the inside of the table view cell (by loading the cell nib).
They not only look identical, they act identical; the view is a custom UIView subclass with an action method from the switch, and when I click the switch, it triggers that action method, both within the table and in the view outside it.
The view was designed entirely in the nib, and absolutely no code was repeated.
That seems to be the sort of thing you want to do. So, I encourage you, don't misuse table view cells; do this with an ordinary view that can live happily inside a cell or outside it.
Related
Let's say I have SomeViewController: UIViewController, and I have a custom view CustomView: UIView, defined as a XIB, that I want to display. This custom view will be reused in other view controllers and even multiple times in the same view controller.
class CustomView: UIView {
#IBOutlet public var label: UILabel!
}
The way I have always added this view has been:
class UIExamples: UIViewController {
#IBOutlet private var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
let customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.frame = myView.bounds
myView.addSubview(customView)
}
}
Let's say that later on I want to modify something about the CustomView via a public property label.
I could do it inside viewDidLoad ONLY BECAUSE I have access to customView, but what if I want to change it in some other function? What I have seen is that one would have to do
let customView = myView.subviews[0] as! CustomView
customView.label.text = "some text"
which does not look right.
So, I thought the right way should be this:
class UIExamples: UIViewController {
#IBOutlet public var customView: CustomView! // Now this is always a CustomView type
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.label.text = "some text" // DOES NOT WORK!
}
}
That last line customView.label.text does not work. In fact, the label is not even seen on the screen. What am I doing wrong?
OK, didn't read (or maybe was reading before edit) that you use xib. If ViewController is created from xib with label in it this will be correct way:
set myView class in xib here:
and then connect IBOutlet (remove current one from xib here:
and then from code).
Now myView.label.text = "some text" should work without further issues.
Good luck!
If you create your view from code do it in this manner:
class UIExamples: UIViewController {
#IBOutlet private var myView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
// Assume makeViewFromNib returns the view [0] in the Nib.
myView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
myView.frame = view.bounds
view.addSubview(myView)
}
}
Because you already have property storing this view in your view controller it's unnecessary to dig inside subviews, it will work like that
myView.label.text = "some text"
And reason for
customView = makeViewFromNib(nib: "\(CustomView.self)", owner: self) as! CustomView
customView.label.text = "some text"
isn't working is because it's completely new view that wasn't added to your view controller subviews (also frame wasn't set BTW). And because you changed value of your customView property it's now not pointing to old instance of view, that is present in subviews (you can still see that "old one" but not change it).
But I really recommend to use pointer created once, as correct class to avoid casting. (Or creating view directly in xib / storyboard, otherwise #IBOutlet is not necessary)
Posting my own answer.
Create the XIB file.
Create the UIView subclass Swift file.
Under the XIB file owner's Identify Inspector custom class field, type in the UIView subclass name (your custom view).
Under the XIB file owner's Connections Inspector, make sure all IBOutlets in the Swift file are connected.
Add a view to the view controller and under its Identify Inspector custom class type, specify the custom class name.
Important:
* In your XIB swift file, you have to properly load the XIB content view.
...
/// Initializer used by Interface Builder.
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
/// Initializer used programmatically.
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
...
func configure() {
let contentView = // here use many of the functions available on the internet to
// load a view from a nib.
// Then add this view to the view hierarchy.
addSubview(contentView)
}
I have a subclass of UIView (MyView) that I've hooked up to a NIB file.
class MyView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var tableView: UITableView!
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
let nibName = "MyView"
private func setup() {
let bundle = Bundle.init(for: type(of: self))
bundle.loadNibNamed(nibName, owner: self, options: nil)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.addSubview(contentView)
}
}
This view is simple; containing only one UITableView (or tableView). However, I want to add a UITableViewCell with a UILabel to tableView but the storyboard is not letting me do this.
I understand that MyView is not a view controller and therefore should not (if following an MVC pattern) implement the various table view data source / delate methods, but, still, why can I not add this table view cell to the table view within this custom view?
My aim was to then have some UIViewController subclass that has an instance of MyView, i.e.
var myView = MyView(),
which it then controls the datasource and delegate methods for, i.e.
myView.tableView.dataSource = self.
Finally, I've attached a screenshot showing that I am unable to add this table view cell to the table view.
You cannot add prototype cells to table views in xib files - only in storyboards.
You can create your table view in one xib, and your table view cell in another xib, if you want.
Or, you can create a second storyboard that would contain a table view and it would support cell prototypes.
I'm interested in creating a view that contains multiple features that users can scroll down and see i.e. pictures, description, comments, carousels etc. I am aware that the UICollectionView is able to provide this type of layout. I initially thought UITableViews would be the best approach.
I have looked at several tutorials and GitHub repos but majority of them just use a UICollectionView in a standard grid layout. I've also looked at IGListKit used by Instagram and some the tutorials linked to it.
I'm aiming to get something like the KitchenStories app:
I was wondering if someone could advice me in terms of the direction and approach best for this.
Don't try to do too much with any single view, even a UICollectionView.
The screen you've shown has a UITabBarController manage its top-level arrangement. The currently selected tab (“Home”) has a UINavigationController managing its content.
On the top of the navigation stack is, probably, a collection view or a table view. Either could be used here because the elements are visually laid out as screen-width rows in a stack. A table view is simpler because then you don't have to worry about setting up the layout.
The table view has several visible rows, each different:
The title/image row (“Easy seafood paella”)
The ratings row
The export row (hearts / save / share)
The comments row
The creator row (I assume, since it looks like it's probably a headshot and a name)
And there are probably even more unique rows out of view.
In your storyboard, you can design each of these rows as a prototype row in the table view controller's scene. Or you can design the table view with static content rows, which is easier if you won't need to change the order of the rows or duplicate any rows at run time.
“But Rob,” you say, “I can't fit all those rows into the table view in my storyboard!” Make the storyboard scene taller. UIKit will resize it at run time to fit the device screen.
In each row, drag in and arrange whatever subviews you need for that row's data. For example, the title/image row needs a UIImageView and a UILabel. The ratings row needs a label, probably a custom view to display and edit the stars, and perhaps a stack view for layout.
For each row, you'll need a separate subclass of UITableViewCell with outlets to that row's views. To pass the data to each cell for display, make each cell conform to a protocol:
protocol RecipeUsing {
var recipe: Recipe? { get set }
}
Then, in your table view controller, you set it like this if you're using static content:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if let user = cell as? RecipeUsing {
user.recipe = recipe
}
return cell
}
You'll need a RecipeTitleImageCell with outlets to the UIImageView and UILabel. Something like this:
class RecipeTitleImageCell: UITableViewCell, RecipeUsing {
#IBOutlet var label: UILabel!
// UITableViewCell has an imageView property that's not an outlet 😭
#IBOutlet var myImageView: UIImageView!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
label.text = recipe.title
myImageView.image = recipe.image
}
}
}
And for the ratings row, you'll want something like this:
class RecipeRatingsCell: UITableViewCell, RecipeUsing {
#IBOutlet var ratingControl: RatingControl!
#IBOutlet var label: UILabel!
var recipe: Recipe? {
didSet {
guard let recipe = recipe else { return }
ratingControl.rating = recipe.ratings.reduce(0, +) / recipe.Double(ratings.count)
if ratings.count < 5 { label.text = "Too few ratings" }
else { label.text = "\(ratings.count) ratings" }
}
}
}
I'm trying to make UIViews which each contain different statements of text (In UITextViews). There can be a varying number of views and each statement can be different in length. I make these views using
let newView = DragView(heightOfView: ???, viewNumber: i, heightFromTop: currentHeightForThings)
In the DragView class I then access the statement using the viewNumber and put the statement in the text label in a nib file I've made.
My issue is I have nothing to put in heightOfView. The height I want is the height of the textLabel which varies depending on how many lines are in the textView for the statement. However I can't access this height because the textLabel isn't built yet.
Thanks in advance, I'm new to swift but want to learn fast so I apologise if I'm missing something obvious!
Heres the code I have in the class DragView
class DragView: UIView {
#IBOutlet var dragView: UIView!
#IBOutlet weak var statementLabel: UITextView!
var dropTarget: UIView?
var viewNumber: Int!
init(heightOfView: Int, viewNumber:Int, heightFromTop: Int) {
self.viewNumber = viewNumber
let startingPosition = CGRect(x: Int(widthCentre) - dragViewWidth / 2, y: heightFromTop, width: dragViewWidth, height: heightOfView)
super.init(frame: startingPosition)
NSBundle.mainBundle().loadNibNamed("DragView", owner: self, options: nil)
self.addSubview(self.dragView)
let movingView = MovingView(frame: startingPosition)
self.addSubview(movingView)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Here the movingView is a subview I add over the view to move the view.
You can override the DragView's sizeThatFits method to have it return a height based on the statementLabel's height. You will have to first call sizeToFit on the textView which will set the height for that view, then return a height based on that.
override func sizeThatFits(size: CGSize) -> CGSize {
self.statementLabel.sizeToFit()
return CGSize(
width: self.frame.width,
height: self.statementLabel.frame.height)
}
Additionally, I would recommend looking into sizeThatFits and layoutSubviews if you are going to be doing programatic layout. The sizing and positioning of subviews should be taking place in layoutSubviews rather than init.
I'm building a weather app as a beginner project. Say I wanted a custom view that consisted on many UILabels for temp, humidity, precipitation, etc. The idea is that this custom UIView would be used several times for every city the user has saved. If the user has 3 cities saved, the custom view would have 3 instances.
What is the best way to do this? I'm trying to subclass a UIView. Originally I was overriding drawRect(rect: CGRect) and defining my UILabels there. That just didn't feel right. And it wouldn't get alloc/inited until way later, after I was trying to update the label text in the completion handler on NSURLSession.
Or should I be overriding init() which makes me do this:
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
And I have no idea what that means. Then I'm forced to doing something like this when I try to init with frame on the root VC.
override init(frame: CGRect) { super.init(frame: frame) }
Can someone walk me through the best approach? I have something like below but I get a nil value right when I'm trying to add the UILabels to subview of the custom class.
class ViewTemplate: UIView {
var tempLabel: UILabel!
var humidityLabel: UILabel!
override init () {
tempLabel = UILabel()
tempLabel.frame = CGRectMake(halfScreenWidth - 130, 120, 260, 130)
tempLabel.textColor = UIColor.whiteColor()
tempLabel.backgroundColor = UIColor.clearColor()
// similar stuff for humidityLabel
super.init()
addSubview(tempLabel)
}
override init(frame: CGRect) { super.init(frame: frame) }
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If not quite sure where the nil is coming from. But most importantly, I'm looking for the best practice in doing this.
Thanks!
For creating the individual View:
Create new View file. Subclass of UIView. Be sure to also create the xib file when doing this.
Draw your items in storyboard (labels, etc.) and connect them to the swift file
Be certain that you connect them in storyboard by making the xib file a Custom Class of the swift file you just created.
Instantiate them inside the awakeFromNib() method. Be sure to set their default values for text if they are labels. Otherwise they will come up as nil when instantiated and your app will crash.
import UIKit
class MyView: UIView {
#IBOutlet weak var templabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.tempLabel.text = "95 degrees"
}
For loading into viewcontroller:
1a. Create a viewcontroller that implements UIScrollView.
1b. Place a UIScrollview in your storyboard and connect it to that viewcontroller
class MyWeatherViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
2a. In ViewDidLoad, create instances of the UIView you are looking to create. Populate them individually.
2b. In ViewDidLoad, set the scrollview width to the width of a single View. Set the height to the height of the single View * the number of Views you wish to display
var view1: MyView = MyView()
var view2: MyView = MyView()
let viewArray[MyView!] = [view1, view2]
for items in viewArray {
var frame = scrollView.bounds
//Set the origin of the views
frame.origin.x = 0.0
frame.origin.y = frame.size.height * items
//create a view out of the object provided, define it's frame, and add to scrollview
let viewToAdd = viewArray[items].view
newPageView.frame = frame
scrollView.addSubview(viewToAdd)
//Set up your labels to be displayed. You must do this AFTER you load the load into the scrollview
viewArray[items].tempLabel.text = "105 degrees"
}
Keep in mind that you will want to set up another data model to hold the information that is to be displayed. I would recommend creating an array that holds the information you wish to display, so you can do viewArray[items].tempLabel.text = tempArray[items] with all the labels you wish to set.
This should align them vertically with the ability to scroll and see all items.
This will also let dynamically decide how many views to add based on cities the user has saved. Just modify the logic to read however many cities the user has etc.
You're on the right track.
Declare the properties for the labels. It looks like you've already done that. Personally, I would declare those as constants with let instead of var because you should always have those labels.
Override the init(frame: CGRect) method. Again, it looks like you've already started this. The reason you should override init(frame: CGRect) instead if init() is because init() just calls init(frame: CGRectZero). If you only implement init() then some of your properties may not get initialized (although this is way less of an issue with Swift since you can declare properties as constants so the compiler throws an error). In your initializer you're going to want to init all your labels & set them up right before you call super.init(frame). After the super.init(frame) method is called you should add the labels as subviews of self.
Override layoutSubviews() if you are laying your UI out programmatically. This method is where you will do all the sizing and laying out of your subviews. I do everything this way so it's what I'm most familiar with. If you're using AutoLayout I believe it's best practice to apply those constraints in the class's initializer since they should only ever need to be set up once.
Optionally, you may also want to override sizeThatFits(). This will ensure your view is always the proper size when sizeToFit() is called on it.
This goes not just for UIView but any subclass of it.
You can use a UIViewController extension and initialize a UIView function that takes a UIView. You can use this anywhere in your project.
extension UIViewController {
func setView(_ view: UIView){
view.backgroundColor = UIColor()
view.layer.cornerRadius = 5.0
// customize your view
}
}
Then: Declare you view in your Controller and pass it when the function is called on viewDidLoad()
let myView = UIView()
setView(myView)