11

How can you add a view modifier to a view when it's only available on iOS 14 but your app is available for iOS 13?

For an example, textCase(_). Section headers in iOS 14 are uppercase so to use the case of your header text you should set .textCase(.none) on your Text, but this didn't exist until iOS 14.

Section(header:
    Text("Header")
        .textCase(.none) //FIXME: 'textCase' is only available in iOS 14.0 or newer
        .font(.system(size: 14, weight: .bold))
        .foregroundColor(.secondary)
        .padding(.top, 50)
)

Xcode offers some suggestions:

  • Add if #available version check
  • Add @available attribute

If you use the #available version check, it wraps all of that scoped code with #available, so you'd have to duplicate all of that to add that one line of code. If you use @available you have to duplicate the entire body property or entire struct.

I considered creating my own ViewModifier that would only apply it if iOS 14 but that gives this dreaded error:

Function declares an opaque return type, but the return statements in its body do not have matching underlying types

struct CompatibleTextCaseModifier: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 14.0, *) {
            return content
                .textCase(.none)
        } else {
            return content
        }
    }
}
Jordan H
  • 52,571
  • 37
  • 201
  • 351

2 Answers2

11

Mark body as @ViewBuilder - this will allow track internal different return types automatically, and remove return because explicit return disables view builder wrapper.

So here is fixed variant

struct CompatibleTextCaseModifier: ViewModifier {

    @ViewBuilder
    func body(content: Content) -> some View {
        if #available(iOS 14.0, *) {
            content
                .textCase(.none)
        } else {
            content
        }
    }
}

and usage

Section(header:
    Text("Header")
        .modifier(CompatibleTextCaseModifier())
        .font(.system(size: 14, weight: .bold))
        .foregroundColor(.secondary)
        .padding(.top, 50)
) {
    Text("test")
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This solution worked to silence the error but would remove the header for me in iOS 13. To fix it I replaced the if #available with a guard #available (and flipped the cases) and it worked great! – Chris McElroy Aug 14 '20 at 19:13
  • 1
    Nevermind the guard doesn’t work in iOS 14. Any advice on this would be awesome. – Chris McElroy Aug 15 '20 at 19:07
  • 3
    This solution is perfect for simple cases like this, but what if you have a big project with tens or hundreds of view modifiers that are only available in iOS 14, creating a compatible modifier for each case will be daunting and you will send up with weird view modifier names, I've created a new question for this regard: https://stackoverflow.com/questions/68892142/swiftui-using-view-modifiers-between-different-ios-versions-without-available – JAHelia Aug 24 '21 at 06:30
0

I know this is an old question, but here's a very useful extension for View that works for all versions of Swift that support #available():

extension View {
    func complexModifier<V: View>(@ViewBuilder _ closure: (Self) -> V) -> some View {
        closure(self)
    }
}

And here's how you'd use it:

struct TestView: View {
    var body: some View {
        List(/* ... */) { item in
            /* ... */
        }
        .complexModifier {
            if #available(iOS 15, *) {
                $0.listRowSeparator(.hidden)
            }
            else {
                $0
            }
        }
    }
}

This will set the .listRowSeparator() view modifier for the list for users running iOS 15+ only.

It looks like a hack. It sounds a hack. And guest what? It is a hack. But it works.

lar3ry
  • 510
  • 4
  • 8