2
\$\begingroup\$

Edit: I added another configuration vector to CardView, a size attribute, which may be .small or .medium. As such, I had to refactor the design to create several descendent protocols of CardView for each attribute, and several concrete structs corresponding to each possible combinations of size and style. Here is the updated design:

// MARK: Example
let example: some View = CardView2Factory.create(
    colors: (.red, .blue),
    name: "My Birthday",
    range: .init(start: Date(), duration: 0),
    configuration: (size: .small, style: .inverted)
)

// MARK: Factory

/// A Factory class used to create a customized `CardView`
class CardView2Factory {
    enum Size {
        case small, medium
    }
    
    enum Style {
        case plain, inverted
    }
    
    private init() {}
    
    /// Creates a new CardView.
    ///
    /// The unique combination of the configuration's `size` and `style` attributes correspond one-to-one with an internal View struct conforming to `CardView`
    ///
    /// - Parameters:
    ///   - colors: the tuple of colors of an Event, used to form the stops of the gradient
    ///   - name: the name of an Event
    ///   - range: the range of an Event
    ///   - configuration: a configuration specifying the size and style attributes of the View
    ///
    /// - Returns: A configuration-dependent CardView expressed as a View
    @ViewBuilder
    static func create(colors: (Color, Color), name: String, range: DateInterval, configuration: (size: Size, style: Style)) -> some View {
        switch configuration {
        case (.small, .plain):
            SmallPlainCardView2(colors: colors, name: name, range: range)
            
        case (.small, .inverted):
            SmallInvertedCardView2(colors: colors, name: name, range: range)
            
        case (.medium, .plain):
            MediumPlainCardView2(colors: colors, name: name, range: range)
            
        case (.medium, .inverted):
            MediumInvertedCardView2(colors: colors, name: name, range: range)
        }
    }
}

// MARK: Main Protocol

protocol CardView2: View {
    associatedtype Accent: ShapeStyle & View
    associatedtype Background: ShapeStyle
    
    var colors: (Color, Color) { get }
    var name: String { get }
    var range: DateInterval { get }
    
    var headerTextColor: Color { get }
    var accent: Accent { get }
    var background: Background { get }
}

// MARK: Size Protocols

extension CardView2 {
    fileprivate var dateFormatter: DateFormatter {
    }
    
    fileprivate var dateComponentsFormatter: DateComponentsFormatter {
    }
        
    fileprivate var mainText: Text {
    }
    
    fileprivate var shape: some Shape {
    }
    
    fileprivate var text: some View {
    }
}

protocol SmallCardView2: CardView2 {
}

extension SmallCardView2 {
    var body: some View {
        ViewA(colors, name, range, accent, etc...)
    }
}

protocol MediumCardView2: CardView2 {
}

extension MediumCardView2 {
    var body: some View {
        ViewB(colors, name, range, accent, etc...)
    }
}

// MARK: Style Protocols

protocol PlainCardView2: CardView2 {
}

extension PlainCardView2 {
    var headerTextColor: Color {
    }
    
    var accent: LinearGradient {
    }
    
    var background: Color {
    }
}

protocol InvertedCardView2: CardView2 {
}

extension InvertedCardView2 {
    var headerTextColor: Color {
    }
    
    var accent: Color {
    }
    
    var background: LinearGradient {
    }
}

// MARK: Implementation

fileprivate struct SmallPlainCardView2: SmallCardView2, PlainCardView2 {
    let colors: (Color, Color)
    let name: String
    let range: DateInterval
}

fileprivate struct SmallInvertedCardView2: SmallCardView2, InvertedCardView2 {
    let colors: (Color, Color)
    let name: String
    let range: DateInterval
}

fileprivate struct MediumPlainCardView2: MediumCardView2, PlainCardView2 {
    let colors: (Color, Color)
    let name: String
    let range: DateInterval
}

fileprivate struct MediumInvertedCardView2: MediumCardView2, InvertedCardView2 {
    let colors: (Color, Color)
    let name: String
    let range: DateInterval
}

and here is the corresponding class diagram (albeit in Java):

class diagram


Using SwiftUI, I have multiple different Views which only differ in some style-based variables. So, I created a Protocol CardView to condense all the similarities in one entity. The Protocol itself inherits from View and implements most of the functionality including var body. It also has several associatedtypes.

I want the client to be able to retrieve an instance of a struct that conforms to this Protocol by simply selecting a case from an enum Style. I decided to use the Factory pattern to do so. However, I learned that Protocols can't have static functions themselves so I had to create a new CardViewFactory class that had a static create method with a style: Style parameter. Another concession I had to make was that this factory method returned some View and not a CardView as would have been preferred, due to its associatedtype requirements.

I'm looking for any feedback on the design of my hierarchy and implementation of the Factory pattern. Thanks!

CardViewFactory

class CardViewFactory {
    enum Style {
        case plain, inverted
    }
    
    @ViewBuilder static func create(name: String, range: DateInterval, colors: (Color, Color), style: Style) -> some View {
        switch style {
        case .plain:
            CardViewA(colors: colors, name: name, range: range)
            
        case .inverted:
            CardViewB(colors: colors, name: name, range: range)
        }
    }
}

CardView

protocol CardView: View {
    associatedtype Accent: ShapeStyle & View
    associatedtype Background: ShapeStyle
    
    var colors: (Color, Color) { get }
    var name: String { get }
    var range: DateInterval { get }
    
    var headerTextColor: Color { get }
    var accent: Accent { get }
    var background: Background { get }
}

extension CardView {
    var body: some View {
         EmptyView() // for example
    }
}

Example Implementation

struct CardViewA: CardView {
    let colors: (Color, Color)
    let name: String
    let range: DateInterval
    
    let headerTextColor: Color = .black
    
    var accent: LinearGradient {
        LinearGradient(
            gradient: .init(colors: [colors.0, colors.1]),
            startPoint: .topTrailing,
            endPoint: .bottomLeading
        )
    }
    
    let background: Color = .white
}
\$\endgroup\$

0

Your Answer

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