2

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?

Tim Fuqua
  • 1,635
  • 1
  • 16
  • 25

2 Answers2

1

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")
1

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)
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    Yeah, that's not going to work for me. See, my example was simple just to show the issue. At the heart of it, I don't even have a View to apply that modifier to "one level up". I'm actually creating an Image and passing it through ViewModel initializers. So the argument type on the init is an Image, and I just wanted to make a mock ViewModel that used a system image with that font trick to make it bigger, and that's where I ran into the issue. – Tim Fuqua Feb 18 '22 at 05:56
  • See update with more alternates. – Asperi Feb 18 '22 at 06:10
  • That may work for my simple example, but it still won’t work for a type where the param it expects is Image or Text or whatever. The point is that I need to pass Image or Text, but I just want to apply the view modifier before I do that but then it’s no longer Image or Text. SomeView in the example is just a means to pass an Image via an initializer to show the issue. – Tim Fuqua Feb 18 '22 at 06:30