Writing customized property wrappers for SwiftUI – Donny Wals


It has been some time since I printed my submit that helps you wrap your head round Swift’s property wrappers. Since then, I’ve completed an increasing number of SwiftUI associated work and one problem that I just lately needed to dig into was passing dependencies from SwiftUI’s atmosphere right into a customized property wrapper.

Whereas figuring this out I discovered concerning the DynamicProperty protocol which is a protocol you can conform your property wrappers to. When your property wrapper conforms to the DynamicProperty protocol, your property wrapper will primarily turn into part of your SwiftUI view. Which means your property wrapper can extract values out of your SwiftUI atmosphere, and your SwiftUI view will ask your property wrapper to replace itself each time it is about to guage your view’s physique. You possibly can even have @StateObject properties in your property wrapper to set off updates in your views.

This protocol is not restricted to property wrappers, however for the sake of this submit I’ll discover DynamicProperty within the context of a property wrapper.

Understanding the fundamentals of DynamicProperty

Defining a dynamic property is so simple as conforming a property wrapper to the DynamicProperty protocol. The one requirement that this protocol has is that you just implement an replace technique that is known as by your view each time it is about to guage its physique. Defining such a property wrapper seems to be as follows:

@propertyWrapper
struct MyPropertyWrapper: DynamicProperty {
  var wrappedValue: String

  func replace() {
    // known as each time the view will consider its physique
  }
}

This instance by itself is not very helpful in fact. I will present you a extra helpful customized property wrapper in a second. Let’s go forward and take a second to discover what we have outlined right here first.

The property wrapper that we have outlined right here is fairly easy. The wrapped worth for this wrapper is a string which signifies that it will be used as a string in our SwiftUI view. If you wish to study extra about how property wrappers work, check out this submit I printed on the subject.

There’s additionally an replace technique that is known as each time SwiftUI is about to guage its physique. This perform permits you to replace state that exists exterior to your property wrapper. You’ll usually discover that you just don’t must implement this technique in any respect until you’re doing a bunch of extra advanced work in your property wrapper. I’ll present you an instance of this in the direction of the top of the article.

⚠️ Be aware that I’ve outlined my property wrapper as a struct and never a category. Defining a DynamicProperty property wrapper as a category is allowed, and works to some extent, however in my expertise it produces very inconsistent outcomes and a variety of belongings you would possibly anticipate to work don’t truly work. I’m not fairly positive precisely why that is the case, and what SwiftUI does to interrupt property wrappers that had been outlined as courses. I simply know that structs work, and courses don’t.

One fairly neat factor a few DynamicProperty is that SwiftUI will decide it up and make it a part of the SwiftUI atmosphere. What this implies is that you’ve entry to atmosphere values, and you can leverage property wrappers like @State and @ObservedObject to set off updates from inside your property wrapper.

See also  Utilizing SwiftUI and WidgetKit to Make Your App Content material Indispensable

Let’s go forward and outline a property wrapper that maintain on to some state and updates the view each time this state adjustments.

Triggering view updates out of your dynamic property

Dynamic properties on their very own can’t inform a SwiftUI view to replace. Nevertheless, we will use @State, @ObservedObject, @StateObject and different SwiftUI property wrappers to set off view updates from inside a customized property wrapper.

A easy instance would look slightly bit like this:

@propertyWrapper
struct CustomProperty: DynamicProperty {
    @State non-public var worth = 0

    var wrappedValue: Int {
        get {
            return worth
        }

        nonmutating set {
            worth = newValue
        }
    }
}

This property wrapper wraps an Int worth. Each time this worth receives a brand new worth, the @State property worth is mutated, which can set off a view replace. Utilizing this property wrapper in a view seems to be as follows:

struct ContentView: View {
    @CustomProperty var customProperty

    var physique: some View {
        Textual content("Rely: (customProperty)")

        Button("Increment") {
            customProperty += 1
        }
    }
}

The SwiftUI view updates the worth of customProperty each time a button is tapped. This by itself doesn’t set off a reevaluation of the physique. The explanation our view updates is as a result of the worth that represents our wrapped worth is marked with @State. What’s neat about that is that if worth adjustments for any motive, our view will replace.

A property wrapper like this isn’t significantly helpful, however we will do some fairly neat issues to construct abstractions round completely different sorts of information entry. For instance, you would construct an abstraction round UserDefaults that gives a key path primarily based model of AppStorage:

class SettingKeys: ObservableObject {
    @AppStorage("onboardingCompleted") var onboardingCompleted = false
    @AppStorage("promptedForProVersion") var promptedForProVersion = false
}

@propertyWrapper
struct Setting<T>: DynamicProperty {
    @StateObject non-public var keys = SettingKeys()
    non-public let key: ReferenceWritableKeyPath<SettingKeys, T>

    var wrappedValue: T {
        get {
            keys[keyPath: key]
        }

        nonmutating set {
            keys[keyPath: key] = newValue
        }
    }

    init(_ key: ReferenceWritableKeyPath<SettingKeys, T>) {
        self.key = key
    }
}

Utilizing this property wrapper would look as follows:

struct ContentView: View {
    @Setting(.onboardingCompleted) var didOnboard

    var physique: some View {
        Textual content("Onboarding accomplished: (didOnboard ? "Sure" : "No")")

        Button("Full onboarding") {
            didOnboard = true
        }
    }
}

Anywhere within the app that makes use of @Setting(.onboardingCompleted) var didOnboard will robotically replace when the worth for onboarding accomplished in person defaults up to date, no matter the place / how this occurred. That is precisely the identical as how @AppStorage works. In truth, my customized property wrapper depends closely on @AppStorage below the hood.

My SettingsKeys object wraps up the entire completely different keys I need to write to in UserDefaults, the @AppStorage property wrapper permits for straightforward statement and makes it in order that I can outline SettingsKeys as an ObservableObject with none troubles.

By implementing a customized get and set on my Setting<T>‘s wrappedValue, I can simply learn values from person defaults, or write a brand new worth just by assigning to the suitable key path in SettingsKeys.

Easy property wrappers like these are very helpful once you need to streamline a few of your knowledge entry, or if you wish to add some semantic that means and ease of discovery to your property wrapper.

See also  android - Flutter Speech to Textual content Provides a touch for numbers

To see some extra examples of straightforward property wrappers (together with a person defaults one which impressed the instance you simply noticed), check out this submit from Dave Delong.

Utilizing SwiftUI atmosphere values in your property wrapper

Along with triggering view updates by way of a few of SwiftUI’s property wrappers, it’s additionally attainable to entry the SwiftUI atmosphere for the view that your property wrapper is utilized in. That is extremely helpful when your property wrapper is extra advanced and has a dependency on, for instance, a managed object context, a networking object, or comparable objects.

Accessing the SwiftUI atmosphere from inside your property wrapper is completed in precisely the identical method as you do it in your views:

@propertyWrapper
struct CustomFetcher<T>: DynamicProperty {
    @Setting(.managedObjectContext) var managedObjectContext

    // ...
}

Alternatively, you possibly can learn atmosphere objects that had been assigned via your view with the .environmentObject view modifier as follows:

@propertyWrapper
struct UsesEnvironmentObject<T: ObservableObject>: DynamicProperty {
    @EnvironmentObject var envObject: T

    // ...
}

We are able to use the atmosphere to conveniently go dependencies to our property wrappers. For instance, let’s say you’re constructing a property wrapper that fetches knowledge from the community. You may need an object named Networking that may carry out community calls to fetch the information you want. You can inject this object into the property wrapper via the atmosphere:

@propertyWrapper
struct FeedLoader<Feed: FeedType>: DynamicProperty {
    @Setting(.community) var community

    var wrappedValue: [Feed.ObjectType] = []
}

The community atmosphere secret is a customized key that I’ve added to my SwiftUI atmosphere. To study extra about including customized values to the SwiftUI atmosphere, check out this tip I posted earlier.

Now that we have now this property wrapper outlined, we want a method to fetch knowledge from the community and assign it to one thing so as to replace our wrapped worth. To do that, we will implement the replace technique that permits us to replace knowledge that’s referenced by our property wrapper if wanted.

Implementing the replace technique on your property wrapper

The replace technique that’s a part of the DynamicProperty protocol permits you to reply to SwiftUI’s physique evaluations. Each time SwiftUI is about to guage a view’s physique, it can name your dynamic property’s replace technique.

💡 To study extra about how and when SwiftUI evaluates your view’s physique, check out this submit the place I discover physique analysis in depth.

As talked about earlier, you received’t usually have a must implement the replace technique. I’ve personally discovered this technique to be useful each time I wanted to kick off some sort of knowledge fetching operation. For instance, to fetch knowledge from Core Knowledge in a customized implementation of @FetchRequest, or to experiment with fetching knowledge from a server. Let’s broaden the FeedLoader property wrapper from earlier a bit to see what an information loading property wrapper would possibly appear like:

@propertyWrapper
struct FeedLoader<Feed: FeedType>: DynamicProperty {
    @Setting(.community) var community
    @State non-public var feed: [Feed.ObjectType] = []
    non-public let feedType: Feed
    @State var isLoading = false

    var wrappedValue: [Feed.ObjectType] {
        return feed
    }

    init(feedType: Feed) {
        self.feedType = feedType
    }

    func replace() {
        Job {
            if feed.isEmpty && !isLoading {
                self.isLoading = true
                self.feed = strive await community.load(feedType.endpoint)
                self.isLoading = false
            }
        }
    }
}

It is a very, quite simple implementation of a property wrapper that makes use of the replace technique to go to the community, load some knowledge, and assign it to the feed property. We now have to be sure that we solely load knowledge if we’re not already loading, and we additionally must verify whether or not or not the feed is at the moment empty to keep away from loading the identical knowledge each time SwiftUI decides to re-evaluate our view’s physique.

See also  ios - Situation with firebase integration in Goal C venture

This in fact begs the query, must you use a customized property wrapper to load knowledge from the community? And the reply is, I’m unsure. I’m nonetheless closely experimenting with the replace technique, its limitations, and its advantages. One factor that’s necessary to understand is that replace known as each time SwiftUI is about to guage a view’s physique. So synchronously assigning a brand new worth to an @State property from inside that technique might be not the very best thought; Xcode even reveals a runtime warning once I do that.

At this level I’m pretty positive Apple supposed replace for use for updating or studying state exterior to the property wrapper moderately than it getting used to synchronously replace state that’s inner to the property wrapper.

However, I’m constructive that @FetchRequest makes heavy use of replace to refetch knowledge each time its predicate or type descriptors have modified, in all probability in a considerably comparable method that I’m fetching knowledge from the community right here.

Abstract

Writing customized property wrappers on your SwiftUI views is a enjoyable train and it’s attainable to jot down some very handy little helpers to enhance your expertise whereas writing SwiftUI views. The incontrovertible fact that your dynamic properties are linked to the SwiftUI view lifecycle and atmosphere is tremendous handy as a result of it permits you to set off view updates, and skim values from SwiftUI’s atmosphere.

That stated, documentation on among the particulars surrounding DynamicProperty is severely missing which signifies that we will solely guess how mechanisms like replace() are imagined to be leverages, and why structs work completely high-quality as dynamic properties however courses don’t.

These are some factors that I hope to have the ability to broaden on sooner or later, however for now, there are positively nonetheless some mysteries for me to unravel. And that is the place you are available in! When you’ve got any additions for this posts, or if in case you have a stable understanding of how replace() was meant for use, be happy to ship me a Tweet. I’d love to listen to from you.



Leave a Reply