Tim Fuqua
Tim Fuqua

Reputation: 1705

In SwiftUI, is there a way to preserve the type when applying a view modifier?

My question is simply as the title states. I want to take some View, apply a view modifier to it, and still be able to keep it as that original type instead of it becoming a some View.

Say we have a simple view like this:

struct SomeView: View {
    let image: Image

    var body: some View {
        image
    }
}

In the PreviewProvider, we can test this out with a system image from SF Symbols:

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SomeView(image: Image.init(systemName: "pawprint"))
                .previewDisplayName("pawprint")
        }
        .previewLayout(.sizeThatFits)
    }
}

And it works:

pawprint

Now let's try to add a second preview using the same SF Symbol but applying the .font view modifier to increase the size:

    static var previews: some View {
        Group {
            SomeView(image: Image.init(systemName: "pawprint"))
                .previewDisplayName("SomeView pawprint")

            // Error: Cannot convert value of type 'some View' to expected argument type 'Image'
            SomeView(image: Image.init(systemName: "pawprint").font(.system(size: 64)))
                .previewDisplayName("SomeView bigPawprint")
        }
        .previewLayout(.sizeThatFits)
    }

Yep, that's about right, because the view modifier .font returns the opaque type some View. But if we apply the "Fix-it button" suggestion to force-cast (sure, let's do that, for science), then it compiles, but crashes the previewer. Also crashes if we try to run in a simulator, so it's not just a Previewer bug.

And yet, there's no problem at all just displaying it as some View:

        Group {
            SomeView(image: Image.init(systemName: "pawprint"))
                .previewDisplayName("SomeView pawprint")

            // Crashes
            // SomeView(image: Image.init(systemName: "pawprint").font(.system(size: 64)) as! Image)
                .previewDisplayName("SomeView big pawprint")

            // Works
            Image.init(systemName: "pawprint").font(.system(size: 64))
                .previewDisplayName("View Modifier big pawprint")
        }
        .previewLayout(.sizeThatFits)

View modifier big pawprint

So, how can I do something like this on a View where I apply a view modifier, but I can still use the original View type?

Upvotes: 4

Views: 1521

Answers (2)

Asperi
Asperi

Reputation: 257789

The .font modifier is applied to all child views, so possible approach is just move it a level up, like

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SomeView(image: Image(systemName: "pawprint"))
                .previewDisplayName("SomeView pawprint")

            SomeView(image: Image(systemName: "pawprint"))
                .font(.system(size: 64))                     // << here !!
                .previewDisplayName("SomeView bigPawprint")
        }
        .previewLayout(.sizeThatFits)
    }
}

demo

Tested with Xcode 13.2 / iOS 15.2

Alternate 1: If it is only about image mocking and SomeView is restricted to Image only, then it is possible to use configured UIImage, like

SomeView(image: Image(uiImage: UIImage(systemName: "pawprint", withConfiguration: UIImage.SymbolConfiguration(pointSize: 64))!))
    .previewDisplayName("SomeView bigPawprint")

Alternate 2: Use generics, like

struct SomeView<V: View>: View {
    let image: V

    var body: some View {
        image
    }
}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            SomeView(image: Image.init(systemName: "pawprint"))
                .previewDisplayName("SomeView pawprint")

            // this works
            SomeView(image: Image.init(systemName: "pawprint").font(.system(size: 64)))
                .previewDisplayName("SomeView bigPawprint")
        }
        .previewLayout(.sizeThatFits)
    }
}

Upvotes: 1

user652038
user652038

Reputation:

No. And because most of the view modifier types are unknown to us (such as whatever backs the font modifier), you'll have to emulate ModifiedContent using a closure, not a Modifier instance.

struct SomeView<ModifiedImage: View>: View {
  let image: Image
  let modify: (Image) -> ModifiedImage

  var body: some View {
    modify(image)
  }
}

extension SomeView where ModifiedImage == Image {
  init(image: Image) {
    self.init(image: image) { $0 }
  }
}
SomeView(image: .init(systemName: "pawprint"))
  .previewDisplayName("SomeView pawprint")

SomeView(image: .init(systemName: "pawprint")) {
  $0.font(.system(size: 64))
}
.previewDisplayName("SomeView bigPawprint")

Upvotes: 1

Related Questions