Search code examples
iosswiftuiviewbuilder

How to apply side effects in a swiftui ViewBuilder


I am trying to render a simple Text() and apply an arbitrary number of different styles . This is my code:

struct Cell {
    var content: String
    var style: TextStyle
}

@ViewBuilder
func styledText(cell: Cell) -> some View {
    if (cell.style.contains(TextStyle.rtl)) {
        Text(cell.content)
            .frame(maxWidth: .infinity, alignment: .leading)
            .environment(\.layoutDirection, .rightToLeft)
            .lineSpacing(10)
    } else if (cell.style.contains(TextStyle.customFont)) {
        Text(cell.content).font(.custom("MyFont", size: 19))
    } else if (cell.style.contains(TextStyle.pinkColour)) {
        Text(cell.content).foregroundStyle(Color(UIColor.systemPink))
    } else {
        Text(cell.content)
    }
}

You can see from the above that only one branch can execute in the above function. But in my app, several styles could be applied (e.g. BOTH customFont and pinkColour). How do I achieve this?

What I tried

@ViewBuilder
func styledText(cell: Cell) -> some View {
    var text = Text(cell.content)
    if (cell.style.contains(TextStyle.rtl)) {
        text = text
            .frame(maxWidth: .infinity, alignment: .leading)
            .environment(\.layoutDirection, .rightToLeft)
            .lineSpacing(10)
    } else if (cell.style.contains(TextStyle.customFont)) {
        text = text.font(.custom("MyFont", size: 19))
    } else if (cell.style.contains(TextStyle.pinkColour)) {
        text = text.foregroundStyle(Color(UIColor.systemPink))
    }
    text
}

As far as I understand (I'm new to Swift), every branch must result in a return value so the above does not work.

I also tried not using @ViewBuilder at all, but I cannot get my code to type check. I get errors such as Cannot assign value of type 'some View' (result of 'Self.font') to type 'some View' (type of 'text')

What am I missing?


Solution

  • You can extract a view modifier for each style, which will take a Bool and decide whether to return a modified view, or self.

    @ViewBuilder
    func styledText(cell: Cell) -> some View {
        Text(cell.content)
            .isRtl(cell.style.contains(.rtl))
            .isCustomFont(cell.style.contains(.customFont))
            .isPinkColor(cell.style.contains(.pinkColor))
    }
    
    extension View {
        @ViewBuilder
        func isRtl(_ enabled: Bool) -> some View {
            if enabled {
                self.frame(maxWidth: .infinity, alignment: .leading)
                    .environment(\.layoutDirection, .rightToLeft)
                    .lineSpacing(10)
            } else {
                self
            }
        }
        
        @ViewBuilder
        func isCustomFont(_ enabled: Bool) -> some View {
            if enabled {
                self.font(.custom("MyFont", size: 19))
            } else {
                self
            }
        }
        
        @ViewBuilder
        func isPinkColor(_ enabled: Bool) -> some View {
            if enabled {
                self.font(.custom("MyFont", size: 19))
            } else {
                self.foregroundStyle(.pink)
            }
        }
    }
    

    Alternatively, always apply all the modifiers, and use some default values when particular TextStyles are not in style.

    func styledText(cell: Cell) -> some View {
        let isRtl = cell.style.contains(.rtl)
        let isCustomFont = cell.style.contains(.customFont)
        let isPink = cell.style.contains(.pinkColor)
        Text(cell.content)
            .frame(maxWidth: isRtl ? .infinity : nil, alignment: isRtl ? .leading : .center)
            .environment(\.layoutDirection, isRtl ? .rightToLeft : .leftToRight)
            .lineSpacing(isRtl ? 10 : 3)
            .font(isCustomFont ? .custom("MyFont", size: 19) : .body)
            .foregroundStyle(isPink ? AnyShapeStyle(.pink) : AnyShapeStyle(.opacity(1.0)))
    }