0
\$\begingroup\$

I am learning Swift & SwiftUI after 15 years doing VBScript & PowerShell. My current learning project is Stewart Lynch's BookList based on SwiftData. And as part of that project he has a View to show & set the book rating, with the code to just show a static presentation done in the BookListView.

As an extra exercise I reworked his "picker" to allow it to also be used to show a static presentation of the current rating, which is working as I wanted. But now I want to get some feedback on that effort. Is it "idiomatic" SwiftUI, or is there a better way to approach the problem? Would two different views where each is a simple as it can be for it's single purpose be better? Or is this within a generally accepted range of "related purposes"? I know this opens the whole "opinion" can of worms, but I am actually hoping to get a diversity of opinion with some technical reasoning behind those opinions, because I think I can learn as much or more from that as I can from doing the code itself.

Also, one thing I am contemplating now is how to deal with error conditions. If none of the three optionals is provided I will probably return an empty view, and if the mutable optional or the max optional is provided without the counterpart I will probably throw an error. Likely also if I have the max provided with the current optional. But this is where I started to question how idiomatic this is, because in PowerShell you can do a parameter set, and any mix of parameters that doesn't meet any defined parameter set throws automatically. Since Swift doesn't (to my knowledge) offer that option, I have to do all that error checking myself, which makes me think two views with no mutually exclusive optionals may be the more idiomatic approach. But I think the entire process has been valuable, because I learned a lot about manual Inits and also how optionals work, and if it turns out I also detected a bit of a code smell in my approach here, well that's super valuable too.

// https://gist.github.com/StewartLynch/03372c873fef568e0a613a968adbae69

import SwiftUI

public struct RatingView: View {
    let currentRating: Int?
    let maxRating: Int?
    @Binding var mutableRating: Int?
    let width:Int
    let color: UIColor
    let sfSymbol: String
    
    public init(
        currentRating: Int? = nil,
        maxRating: Int? = nil,
        mutableRating: Binding<Int?> = Binding.constant(nil),
        width: Int = 20,
        color: UIColor = .systemYellow,
        sfSymbol: String = "star"
    ) {
        self.currentRating = currentRating
        self.maxRating = maxRating
        self._mutableRating = mutableRating
        self.width = width
        self.color = color
        self.sfSymbol = sfSymbol
    }

    public var body: some View {
        if maxRating == nil {
            if currentRating != nil {
                HStack {
                    ForEach(0..<currentRating!, id: \.self) { _ in
                        Image(systemName: sfSymbol)
                            .resizable()
                            .scaledToFit()
                            .fillImage(true)
                            .foregroundStyle(Color(color))
                    }
                }
                .frame(width: CGFloat(currentRating! * width))
            }
        } else {
            HStack {
                Image(systemName: sfSymbol)
                    .resizable()
                    .scaledToFit()
                    .symbolVariant(.slash)
                    .foregroundStyle(Color(color))
                    .onTapGesture {
                        withAnimation{
                            mutableRating = nil
                        }
                    }
                    .opacity(mutableRating == 0 ? 0 : 1)
          
                ForEach(0..<maxRating!, id: \.self) { rating in
                    Image(systemName: sfSymbol)
                        .resizable()
                        .scaledToFit()
                        .fillImage(correctImage(for: rating))
                        .foregroundStyle(Color(color))
                        .onTapGesture {
                            withAnimation{
                                mutableRating = rating + 1
                            }
                        }
                }
            }.frame(width: CGFloat(maxRating! * width))
        }
    }
    
    func correctImage(for rating: Int) -> Bool {
        if let mutableRating, rating < mutableRating {
            return true
        } else {
            return false
        }
    }
}

struct FillImage: ViewModifier {
    let fill: Bool
    func body(content: Content) -> some View {
        if fill {
            content
                .symbolVariant(.fill)
        } else {
            content
        }
    }
}

extension View {
    func fillImage(_ fill: Bool) -> some View {
        modifier(FillImage(fill: fill))
    }
}

#Preview ("mutating") {
    struct PreviewWrapper: View {
        @State var currentRating: Int? = 2
        
        var body: some View {
            RatingView(
                maxRating: 5,
                mutableRating: $currentRating,
                width: 30,
                color: .red,
                sfSymbol: "heart"
            )
        }
    }
    return PreviewWrapper()
}

#Preview ("non mutating") {
    struct PreviewWrapper: View {
        
        var body: some View {
            RatingView(
                currentRating: 3,
                width: 20,
                color: .red,
                sfSymbol: "heart"
            )
        }
    }
    return PreviewWrapper()
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.