-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dashboard Card created for personal project #10
base: main
Are you sure you want to change the base?
Conversation
let image1 = UIImageView(image: #imageLiteral(resourceName: "image1")) | ||
let image2 = UIImageView(image: #imageLiteral(resourceName: "image2")) | ||
let title = UILabel() | ||
let subTitle = UILabel() | ||
|
||
dashboardCard.addSubview(image1) | ||
dashboardCard.addSubview(image2) | ||
dashboardCard.addSubview(title) | ||
dashboardCard.addSubview(subTitle) | ||
|
||
dashboardCard.translatesAutoresizingMaskIntoConstraints = false | ||
image1.translatesAutoresizingMaskIntoConstraints = false | ||
image2.translatesAutoresizingMaskIntoConstraints = false | ||
title.translatesAutoresizingMaskIntoConstraints = false | ||
subTitle.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
view.backgroundColor = .lightGray | ||
|
||
title.font = UIFont.preferredFont(forTextStyle: .title1) | ||
subTitle.font = UIFont.preferredFont(forTextStyle: .subheadline) | ||
title.text = "$1200" | ||
subTitle.text = "45% This week" | ||
subTitle.textColor = .gray | ||
|
||
|
||
|
||
NSLayoutConstraint.activate([ | ||
dashboardCard.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 8), | ||
dashboardCard.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), | ||
dashboardCard.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 8), | ||
|
||
image1.leadingAnchor.constraint(equalTo: dashboardCard.leadingAnchor, constant: 20), | ||
image1.topAnchor.constraint(equalTo: dashboardCard.topAnchor, constant: 20), | ||
image1.heightAnchor.constraint(equalToConstant: 50), | ||
image1.widthAnchor.constraint(equalToConstant: 50), | ||
|
||
title.leadingAnchor.constraint(equalTo: image1.leadingAnchor), | ||
title.topAnchor.constraint(equalTo: image1.bottomAnchor, constant: 20), | ||
|
||
image2.bottomAnchor.constraint(equalTo: title.centerYAnchor), | ||
image2.trailingAnchor.constraint(equalTo: dashboardCard.trailingAnchor, constant: -50), | ||
image2.widthAnchor.constraint(equalToConstant: 75), | ||
image2.heightAnchor.constraint(equalToConstant: 50), | ||
|
||
subTitle.leadingAnchor.constraint(equalTo: image1.leadingAnchor), | ||
subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 8), | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this setup should be in DashboardCard
, part of its private implementation details. The images and labels should be properties of the card.
The DashboardCard
should expose a public API for providing images and text that can be applied to its subviews. There are two ways of going about it:
-
The properties for images and labels are
private
, andDashboardCard
provides a method likefunc configure(with model: DashboardCard.Model)
, which would take aModel
struct with UIImage and String properties thatDashbaordCard
can decorate itself with. -
The properties for images and labels are not private, and the object using the card (in this case, the view controller) sets their properties:
card.someImageView.image = blah
,card.someLabel.text = blah
, etc.
There are arguments for and against both options. What do you think? Which one would you argue for, and why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with the first option because as a developer I find it easier to fill in the preset items that are required when initializing the component. So in my DashboardCard file should I make the properties private?
I was trying to figure out how to change the background color of the Card itself I tried it in both the ViewController and the DashboardCard and neither is making the background color a different color. What am I missing? I'm sure it's something simple that I just can't see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi. I need to clarify what I meant by a "configure" method. What you have in your updated code isn't what I was recommended. When you're writing a custom view, there are — very, very roughly speaking — two kinds of configuration:
-
Initial setup done once. - This is stuff like adding subviews, establishing AutoLayout constraints, choosing fonts and colors, etc. These are things that a view would set up all by itself, either during
init
or duringawakeFromNib
, etc. What's important to note here is that this is setup that is common to any content the view might display. For example, if you were writing a Twitter client, the layout code in a tweet cell that puts the avatar over here and the user name over there is something you would set once, even though that tweet cell might get re-used many times to display many different tweets. -
Updating the view to display new content. - This is stuff that happens once, twice, or a hundred times during the lifetime of the view, such as: setting the
image
property on a UIImageView, setting thetext
of a label, etc. In my example of the tweet cell, this is setting theimage
property on the avatar image view, or thetext
property of a user name label. This happens frequently.
So with your Dashboard card, you also have two kinds of setup. Setup Number 1 is what you currently have implemented in private func configure()
. There's also additional setup needed to apply a particular image or text to the DashboardCard's subviews. Currently you are doing this in init
, but that's discouraged. It's better to design the API of your view to be initialized once and re-used as needed.
Here's a stubbed-in version explaining what I mean:
class DashboardCard: UIView {
struct Model {
let image1: UIImage
let image2: UIImage
let title: String
let subtitle: String
}
private let imageView1 = UIImageView()
private let imageView2 = UIImageView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
initialSetup()
}
func configure(with model: Model) {
imageView1.image = model.image1
imageView2.image = model.image2
titleLabel.text = model.title
subtitleLabel.text = model.subtitle
}
private func initialSetup() {
// add subviews
// set up layout constraints
// set background colors, etc.
}
}
What this setup allows you to do is make all the details that only DashboardCard should need to know about as private
matters. It lets another object that needs to use a DashboardCard just do the following:
class SomeViewControllerSomewhere: UIViewController {
@IBOutlet private var dashboardCard: DashboardCard!
func didRefreshAndGetSomeDataOrWhatever(data: SomeModelType) {
let cardModel = DashboardCard.Model(image1: data.i1, image2: data.i2, title: data.t, subtitle: data.s)
dashboardCard.configure(with: cardModel)
}
}
What you can see here is that SomeViewControllerSomewhere
doesn't need to know anything about the private details of DashboardCard or it's subviews. All it needs, other than adding a DashboardCard
to it's view hierarchy, is to call configure(with: DashboardCard.Model)
on the card, passing the model values it wants the card to present. Don't pass actual views (don't pass image views or labels). Instead, pass values: images, strings, bools, that sort of thing.
var image1 = UIImageView() | ||
var image2 = UIImageView() | ||
let title = UILabel() | ||
let subTitle = UILabel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend making these private
unless there's a good reason for another object to be able to reach in and access these things directly.
var image1 = UIImageView() | |
var image2 = UIImageView() | |
let title = UILabel() | |
let subTitle = UILabel() | |
private var image1 = UIImageView() | |
private var image2 = UIImageView() | |
private let title = UILabel() | |
private let subTitle = UILabel() |
let subTitle = UILabel() | ||
|
||
override init(frame: CGRect) { | ||
super .init(frame: frame) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Watch out for little inconsistencies of spacing like this. It can feel frustrating that this matters, but I assure you there are plenty of technical reviewers out there who will notice this kind of thing.
super .init(frame: frame) | |
super.init(frame: frame) |
init(image1: UIImageView, image2: UIImageView, title: String, subTitle: String) { | ||
super .init(frame: .zero) | ||
self.image1 = image1 | ||
self.image2 = image2 | ||
self.title.text = title | ||
self .subTitle.text = subTitle | ||
configure() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my long-winded comment above. This shouldn't take any arguments, just override init(frame:)
init(image1: UIImageView, image2: UIImageView, title: String, subTitle: String) { | |
super .init(frame: .zero) | |
self.image1 = image1 | |
self.image2 = image2 | |
self.title.text = title | |
self .subTitle.text = subTitle | |
configure() | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
configure() | |
} |
layer.cornerRadius = 10 | ||
self.backgroundColor = .systemPink | ||
|
||
self.addSubview(image1) | ||
self.addSubview(image2) | ||
self.addSubview(title) | ||
self.addSubview(subTitle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has a mix of including self.
and excluding it. Make sure you pick one style and stick with it, at least within a file or a feature. My recommendation is to exclude self.
when it can be, that's what most places are doing now.
layer.cornerRadius = 10 | |
self.backgroundColor = .systemPink | |
self.addSubview(image1) | |
self.addSubview(image2) | |
self.addSubview(title) | |
self.addSubview(subTitle) | |
layer.cornerRadius = 10 | |
backgroundColor = .systemPink | |
addSubview(image1) | |
addSubview(image2) | |
addSubview(title) | |
addSubview(subTitle) |
image1.translatesAutoresizingMaskIntoConstraints = false | ||
image2.translatesAutoresizingMaskIntoConstraints = false | ||
title.translatesAutoresizingMaskIntoConstraints = false | ||
subTitle.translatesAutoresizingMaskIntoConstraints = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch. It is really easy to forget to do this with views that are initialized programmatically. I hate that's what UIKit still does.
subTitle.font = UIFont.preferredFont(forTextStyle: .subheadline) | ||
subTitle.textColor = .gray | ||
|
||
NSLayoutConstraint.activate([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, using NSLayoutConstraint.activate
and the anchor APIs. I like to use these, too, I think they're a good blend of ease-of-use and familiarity.
title.leadingAnchor.constraint(equalTo: image1.leadingAnchor), | ||
title.topAnchor.constraint(equalTo: image1.bottomAnchor, constant: 20), | ||
|
||
image2.bottomAnchor.constraint(equalTo: title.centerYAnchor), | ||
image2.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -50), | ||
image2.widthAnchor.constraint(equalToConstant: 75), | ||
image2.heightAnchor.constraint(equalToConstant: 50), | ||
|
||
subTitle.leadingAnchor.constraint(equalTo: image1.leadingAnchor), | ||
subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 8), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason you're not seeing a background color is because the DashboardCard has an ambiguous height, given the constraints you have here. I opened your app in Xcode's visual debugger to help diagnose this:
UIKit can't determine what the height is supposed to be, so it's using 0
, which is the height it's initialized with. The reason the height is ambiguous is because your constraints don't have a contiguous chain of constraints from the top of the card to the bottom of the card. Right now the chain is broken:
- START: top of the card
- top of the card to top of image 1
- bottom of image1 to top of title label
- bottom of title label to top of subtitle label
- BROKEN
- END: bottom of the card
You need a constraint between the bottom of the subtitle label and the bottom of the card.
Description
Fixes # (issue # from issue tracker)
Type of change
Change Status
How Has This Been Tested?
Checklist