31

I get an error "Type 'PlayButtonModifier' does not conform to protocol 'ViewModifier'" and I do not understand why and - even more important - how to do it right.

I simply try to create a ViewModifier for an Image, so that I can use for example .resizable() on it, which is only defined in Image

In the ViewModifier protocol, there's an Typealias for Content defined. My naiv thinking was that this should work:

struct PlayButtonModifier: ViewModifier {
    typealias Content = Image

    func body(content: Content) -> some View {
        content
    }
}

Well, no. Too easy. Same thing happens with implicit type alias for structs:

struct PlayButtonModifier: ViewModifier {
    func body(content: Image) -> some View {
        content
    }
}

Same error.

What is wrong here? How would it be correct?

jboi
  • 11,324
  • 4
  • 36
  • 43
  • Content is opaque typealias here... [this post](https://stackoverflow.com/questions/56833659/what-is-content-in-swiftui) will help you to clarify the area. – Asperi Nov 11 '19 at 20:42
  • Hi @Asperi, the answer behind your link explains very well what happens behind the scenes. Thank you for this. And now the 1 Mio $ question: How can I use it? Apple took all the effort, including even a syntax change in Swift, just to be able to modify standard plain views? Not even Images or Texts? I still believe, there's something in it, that I just have not uncovered, yet. – jboi Nov 11 '19 at 21:31
  • All modifiers actually a functions, ie. .resizable(). ViewModifier is generic mechanism for generic View modification. For specific modifications you can create specific function, calculable property, dedicated View, etc. the only one requirement - those should generate some View as output. – Asperi Nov 12 '19 at 03:50

2 Answers2

31

In this case where the modification is specific to a particular view type, Image say, you could just add an extension on that view type directly:

extension Image {
    func myImageModifier() -> some View {
        self
            .resizable()
            .aspectRatio(1.0, contentMode: .fit)
            .clipShape(Circle())
   }
}

A full playground text example follows. If you add a cute otter picture in your playground "Resources" folder named "Otter.png" you get a prettier result :)

import PlaygroundSupport
import SwiftUI

let image = (UIImage(named: "Otter.png") ?? UIImage(systemName: "exclamationmark.square")!)

struct ContentView: View {
    var body: some View {
        VStack {
            Text("hello world")
            Image(uiImage: image)
                .myImageModifier()
        }
    }
}

extension Image {
    func myImageModifier() -> some View {
        self
            .resizable()
            .aspectRatio(1.0, contentMode: .fit)
            .clipShape(Circle())
    }
}

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Dad
  • 6,388
  • 2
  • 28
  • 34
23

thank's to the comments and discussion with Asperi, I finally used the following code snippet. Basically, it's an implementation of ViewModifier, specialised for Images.

protocol ImageModifier {
    /// `Body` is derived from `View`
    associatedtype Body : View

    /// Modify an image by applying any modifications into `some View`
    func body(image: Image) -> Self.Body
}

extension Image {
    func modifier<M>(_ modifier: M) -> some View where M: ImageModifier {
        modifier.body(image: self)
    }
}

Using it is simple:

struct MyImageModifier: ImageModifier {
    func body(image: Image) -> some View {
        image.resizable().scaledToFit()
    }
}

struct MyView: View {
    var body: some View {
        Image(systemName: "play").modifier(MyImageModifier())
    }
}

I'm not 100% satisfied, because the modifier must be defined either to return some View or to return an Image. Both cases have disadvantages and do not perfectly integrate with SwiftUI.

When defining the ImageModifier to return an Image it reduces the possibilities of modifying the image to only the image-specific modifiers (actually resizable()) and when defining it to return some View I can't chain ImageModifiers as the second modifier must be a ViewModifier.

jboi
  • 11,324
  • 4
  • 36
  • 43