英文:
Custom Review star images not showing properly in Swift
问题
我在Storyboard中使用了5个ImageView,并连接了如下的outlets:
@IBOutlet var starRatingImageViews: [UIImageView]!
我从JSON中获取了"rating"的字符串值,例如:
rating = "4.2"
例如,rating = "4.2"
我需要显示4个满星和半颗星,但它没有正确显示。
输出:这里我有 "4.2" 的评分,但星星在输出中显示错误。请指导我如何解决这个问题。
代码: 在这里,user.rating = "4.2"
但在输出中显示错误的星星。在 fillStars
方法中我哪里错了,请指导。
@IBOutlet var starRatingImageViews: [UIImageView]!
if let rating = Float(user.rating ?? "0.0") {
rating.fillStars(in: starRatingImageViews)
}
func fillStars(in imageViews: [UIImageView], withEmptyStar color: UIColor = .darkGray) {
for i in (0 ..< imageViews.count) {
imageViews[i].contentMode = .scaleAspectFit
let review = Int(Float(self))
if (i < review) {
imageViews[i].image = UIImage(systemName: "star.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else {
if (self - Float(review)) > (0.5), i == review {
imageViews[i].image = UIImage(systemName: "star.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else if (self - Float(review)) <= (0.5), (self - Float(review)) > (0.0), i == review {
imageViews[i].image = UIImage(systemName: "star.leadinghalf.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else {
imageViews[i].image = UIImage(systemName: "star")?.withRenderingMode(.alwaysOriginal).withTintColor(color)
}
}
}
}
英文:
I have taken 5 imageviews in storyboard and connected outlets like this
@IBOutlet var starRatingImageViews: [UIImageView]!
and i am getting string value from json for rating like this rating = "4.2"
for eg. rating = "4.2" i need to show 4 full stars and one half star
but its not showing properly
o/p: here i have "4.2" rating but its stars showing wrongly in output. please guide me to fix this issue
code: here user.rating = "4.2"
but showing wrong stars in o/p. where am i wrong in fillStars
method. please do guide.
@IBOutlet var starRatingImageViews: [UIImageView]!
if let rating = Float(user.rating ?? "0.0") {
rating.fillStars(in: starRatingImageViews)
}
func fillStars(in imageViews: [UIImageView], withEmptyStar color: UIColor = .darkGray) {
for i in (0 ..< imageViews.count) {
imageViews[i].contentMode = .scaleAspectFit
let review = Int(Float(self))
if (i < review) {
imageViews[i].image = UIImage(systemName: "star.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else {
if (self - Float(review)) > (0.5), i == review {
imageViews[i].image = UIImage(systemName: "star.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else if (self - Float(review)) <= (0.5), (self - Float(review)) > (0.0), i == review {
imageViews[i].image = UIImage(systemName: "star.leadinghalf.fill")?.withRenderingMode(.alwaysOriginal).withTintColor(CommonColor.yellowColor)
} else {
imageViews[i].image = UIImage(systemName: "star")?.withRenderingMode(.alwaysOriginal).withTintColor(color)
}
}
}
}
答案1
得分: 0
以下是您要的翻译部分:
问题在于您正在使用self - Float(review)
来确定要绘制的星星类型,但self
与您的循环无关,而且您甚至在使用i < review
作为第一个测试时也不一致。您可以让它工作,但比必要的要复杂。问题更加混乱,因为您比需要的情况更多:您只需要绘制一个填充的星星、半填充的星星或空星星。这是3种情况,但您的代码中有4种情况,因为您复制了完整的星星情况。
在评论中,您澄清了let review = Int(Float(self))
是从String
转换而来。因此,让我们看看当self
应该是"4.2"
,而i
为3时会发生什么,这是它错误地计算了部分星星的情况。在这种情况下,review
将为4,因此if i < review
应该 为true
,因为3小于4,所以它应该将imageViews[i]
设置为满星,但实际上并没有这样做。这告诉我self
不是 "4.2"
,因此review
也不是4
。要么打印self
,要么在if
上设置断点,查看self
和review
的实际情况。这可能会显示出您的错误。基于此,我认为您关于self
是什么的假设可能有误。
即使在修复这个问题之后,fillStars
仍然存在一个bug,因为当i
为4时,即最后一个星星,它完全填充了它,但它应该是空的。
尽管如此,fillStars
可以更简单一些。
当我遇到这种问题时,我更喜欢根据当前指示器(星星在您的情况下)需要绘制的百分比来思考,然后使用该结果来决定要执行什么操作,而不是将计算和决策混合在一起。因此,我会修改您的代码,如下所示:
func clamp<T: Comparable>(_ value: T, to range: ClosedRange<T>) -> T {
return max(range.lowerBound, min(range.upperBound, value))
}
func fillStars(
in imageViews: [UIImageView],
withEmptyStar color: UIColor = .darkGray)
{
let review = Float(self) // 现在是否需要这个了?
for i in imageViews.indices
{
imageViews[i].contentMode = .scaleAspectFit
let tintColor: UIColor
let starName: String
// 计算要绘制的当前星星的百分比
let percentOfThisStar = clamp(review - Float(i), to: 0...1)
// 现在使用百分比来选择图像/着色
if percentOfThisStar == 0
{ // 空星星
tintColor = color
starName = "star"
}
else
{
tintColor = CommonColor.yellowColor
starName = percentOfThisStar < 0.5
? "star.leadinghalf.fill" // 部分星星
: "star.fill" // 满星
}
imageViews[i].image = UIImage(systemName: starName)?
.withRenderingMode(.alwaysOriginal)
.withTintColor(tintColor)
}
}
作为建议,我可能会将fillStars
移动到代码库的另一部分。我不太确定self
是什么(Float
?Double
?Decimal
?),除了它是某种数值类型,而且它似乎是在任何数值类型的扩展中的一个方法。我可能会将其转换为自由函数,或者可能会将其转换为包含星星的视图中的方法,并将review
作为参数传递。我的理由仅仅是填充星星与self
可能是什么数值类型没有太多关系。这只是您的应用程序的一个特殊用例,可能只适用于您应用程序的一个特定视图,那么为什么不将其放在该视图中呢?或者作为顶级的应用程序特定函数?显然这是一个判断的问题。编译器并不关心,所以只是根据您和您的团队的需求来决定。
为了更轻松地测试它而不必创建完整的应用程序(并且避免使用模拟器),我在命令行工具中编写了以下代码:
enum StarType: String
{
case empty = "[ ]"
case partial = "[* ]"
case filled = "[**]"
}
func clamp<T: Comparable>(_ value: T, to range: ClosedRange<T>) -> T {
return max(range.lowerBound, min(range.upperBound, value))
}
func fillStars(
in stars: inout [StarType],
from rating: Float)
{
assert(Float(stars.count) >= rating)
for i in stars.indices
{
// 计算要绘制的当前星星的百分比
let percentOfThisStar = clamp(rating - Float(i), to: 0...1)
// 现在使用百分比来设置星星类型
let starType: StarType
if percentOfThisStar == 0
{ // 空星星
starType = .empty
}
else
{
starType = percentOfThisStar < 0.5
? .partial
: .filled
}
// 这可以在`if`中完成,但在此处执行可以更容易地将其重构为UIImageView,如果需要的话。
stars[i] = starType
<details>
<summary>英文:</summary>
The problem is that you're using `self - Float(review)` to figure out what kind of star to draw, but `self` is independent of your loop, and you're not even consistent about it, since you use `i < review` as the first test. You can make that work, but it's harder than it needs to be. The matter is confused somewhat more by having more cases than you need: You either need to draw a filled star, a half-filled star, or an empty star. That's 3 cases, but you have 4 in your code, because you duplicate the full star case.
In comments you clarified that `let review = Int(Float(self))` is converting from `String`. So let's look at what happens when `self` is supposed to be `"4.2"` and `i` is 3, which is where it incorrectly computes the partial star. In this case `review` will be 4, so `if i < review` *should* be `true` since 3 is less than 4, so it should set `imageViews[i]` to a full star, but it doesn't actually do that. This tells me that `self` is *not* `"4.2"`, and therefore `review` is not `4`. Either print `self` or put a break-point on the `if` to see what `self` and `review` actually are. That will probably show you your bug. Based on that, I think your assumptions about what `self` is are likely incorrect somehow.
Even after fixing that, though, `fillStars` still has a bug, because when `i` is 4, that is on the last star, it completely fills it, but it should be empty.
Nonetheless, `fillStars` could be simpler.
When I have problems like this, I prefer to think in terms of calculating the percent of the current indicator (star, in your case) that needs to be drawn, and then use that result to decide what to do, rather than mixing the calculation and decision-making together. So I would modify your code to be something like this:
```swift
func clamp<T: Comparable>(_ value: T, to range: ClosedRange<T>) -> T {
return max(range.lowerBound, min(range.upperBound, value))
}
func fillStars(
in imageViews: [UIImageView],
withEmptyStar color: UIColor = .darkGray)
{
let review = Float(self) // Is this needed now?
for i in imageViews.indices
{
imageViews[i].contentMode = .scaleAspectFit
let tintColor: UIColor
let starName: String
// Calculate the percentage of the current star to draw
let percentOfThisStar = clamp(review - Float(i), to: 0...1)
// Now use the percentage to pick the image/tinting
if percentOfThisStar == 0
{ // Empty star
tintColor = color
starName = "star"
}
else
{
tintColor = CommonColor.yellowColor
starName = percentOfThisStar < 0.5
? "star.leadinghalf.fill" // Partial star
: "star.fill" // Full star
}
imageViews[i].image = UIImage(systemName: starName)?
.withRenderingMode(.alwaysOriginal)
.withTintColor(tintColor)
}
}
As a suggestion, I'd probably move fillStars
to another part of your code base. I'm not exactly sure what self
is (Float
? Double
? Decimal
?), except that it's some numeric type, and it seems fillStars
is a method in an extension on whatever that numeric type is. I'd probably just make it free function instead, or maybe a method in whatever view contains the stars, and pass the review
in as a parameter. My reasoning is just that filling stars doesn't have much to do with being any of the numeric types that self
might be. It's just a special case usage for your app, probably just for one specific view in your app, so why not put it in that view? Or as a top-level app-specific function? Obviously that's a judgement call. The compiler doesn't care, so it's just a matter of what makes the most sense for you and your team.
To make life a little easier to test it without creating a full-on app (and to avoid having to use Simulator), I wrote it like this in a command line tool:
enum StarType: String
{
case empty = "[ ]"
case partial = "[* ]"
case filled = "[**]"
}
func clamp<T: Comparable>(_ value: T, to range: ClosedRange<T>) -> T {
return max(range.lowerBound, min(range.upperBound, value))
}
func fillStars(
in stars: inout [StarType],
from rating: Float)
{
assert(Float(stars.count) >= rating)
for i in stars.indices
{
// Calculate the percentage of the current star to draw
let percentOfThisStar = clamp(rating - Float(i), to: 0...1)
// Now use the percentage to set the star type
let starType: StarType
if percentOfThisStar == 0
{ // Empty star
starType = .empty
}
else
{
starType = percentOfThisStar < 0.5
? .partial
: .filled
}
// This could have been done in the `if`, but doing it here allows
// easier refactoring to UIImageView, if that's desired.
stars[i] = starType
}
}
I renamed review
to be rating
, and pass it in as a parameter. Instead of a filled star, I print [**]
, for a partial star I print [* ]
and for an empty star I print [ ]
.
I drive it with this code:
var stars: [StarType] = .init(repeating: .empty, count: 5)
let rating: Float = 4.2
fillStars(in: &stars, from: rating)
let starStr = stars.map { $0.rawValue }.joined()
print("rating = \(rating), stars = \(starStr)")
These are the results:
<pre>
rating = 4.2, stars = [][][][][* ]
</pre>
Which appears to be the correct result.
As mentioned above, based on what you've described, I think there is at least one bug that happens before calling fillStars
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论