Dynamic Features in Swift [FREE]
source link: https://www.tuicool.com/articles/hit/2QvMjiu
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
As a busy Swift developer, you have needs that are specific to your world yet common to all. You want to create clean-looking code, learn what’s going on in your code at a glance and resolve unforeseen issues quickly.
This tutorial ties together the dynamic and flexible parts of Swift to meet those needs. Using the latest Swift technology, you’ll learn how to customize output to your console, hook into third-party object state changes, and use some sweet syntactical sugar to write cleaner code.
Specifically, you will learn about:
Mirror CustomDebugStringConvertible
Most of all, you’ll have a doggone good time!
This tutorial requires Swift 4.2 or later. Until Xcode 10 is released, you must download the latest Xcode 10 Beta or install the latest Swift 4.2 snapshot .
Also, you must have an understanding of basic Swift types. The Getting to Know Enums, Structs and Classes in Swift tutorial is a great place to start. Although not strictly required, you may want to look into Implementing Custom Subscripts in Swift as well.
Getting Started
Before doing anything, download the starter and final projects by clicking on the Download Materials button at the top or the bottom of the tutorial. Unzip the downloaded file.
You’ll be happy to know that all the code you need to let you focus on learning the dynamic features of Swift is already written for you! Like walking with a friendly guide dog, this tutorial will guide you through everything in the starter code.
Happy Dog
In the starter code directory named DynamicFeaturesInSwift-Starter , you’ll see three playground pages: DogMirror , DogCatcher and KennelsKeyPath . The playground is set to run on macOS. This tutorial is platform-agnostic and only focuses on the Swift language.
Reflecting on Mirror and Debug Output
Whether you’re tracking down an issue or just exploring running code, uncluttered information in the console makes all the difference. Swift offers many ways of customizing console output and capturing crucial events. For customizing output, it doesn’t get any deeper than Mirror. Swift offers more power than the strongest sled dog to pull you out of the icy cold land of confusion!
Siberian Husky Sled Dogs
Before learning more about Mirror
, you’ll first write some customized console output for a type. This will help you more clearly see what’s going on.
CustomDebugStringConvertible
Open DynamicFeaturesInSwift.playground in Xcode and go to the **DogMirror** page.
In honor of all those cute little dogs that get lost, caught by a dog catcher and then reunited with their owners, this page has a Dog
class and DogCatcherNet
class. Focus on the DogCatcherNet
first.
Since the lost doggies out there must be caught and reunited with their owners, dog catchers must be supported. The code you write in the following project will help dog catchers evaluate the quality of nets.
In the playground, look at the following:
enum CustomerReviewStars { case one, two, three, four, five }
class DogCatcherNet { let customerReviewStars: CustomerReviewStars let weightInPounds: Double // ☆ Add Optional called dog of type Dog here init(stars: CustomerReviewStars, weight: Double) { customerReviewStars = stars weightInPounds = weight } }
let net = DogCatcherNet(stars: .two, weight: 2.6) debugPrint("Printing a net: \(net)") debugPrint("Printing a date: \(Date())") print()
The DogCatcherNet
has two properties: customerReviewStars
and its weightInPounds
. Customer review stars reflect the customers’ feelings about the net product. The weight in pounds tells the dog catchers what burden they will experience lugging a net around.
Run the playground. The first two lines you should see are similar to the following:
"Printing a net: __lldb_expr_13.DogCatcherNet" "Printing a date: 2018-06-19 22:11:29 +0000"
As you can see, the debug output in the console prints something related to a net and a date. Bless its heart; the output from the code looks like it was made by a robotic pet. This pet tried hard, but it needs help from us humans. As you can see, it’s printing out extra information such as “__lldb_expr_.” Printing out the date provides something more useful. It’s up in the air as to whether or not this is enough to help you track down a problem that’s been dogging you.
To increase your chances of success, you need to apply some console output customization basics using CustomDebugStringConvertible
magic. In the playground, add the following code right under ☆ Add Conformance to CustomDebugStringConvertible for DogCatcherNet here
:
extension DogCatcherNet: CustomDebugStringConvertible { var debugDescription: String { return "DogCatcherNet(Review Stars: \(customerReviewStars)," + " Weight: \(weightInPounds))" } }
For something small like DogCatcherNet
, a type can conform to CustomDebugStringConvertible
and provide its own debug description using the debugDescription
property.
Run the playground. Except for a date value difference, the first two lines should include:
"Printing a net: DogCatcherNet(Review Stars: two, Weight: 2.6)" "Printing a date: 2018-06-19 22:10:31 +0000"
For a larger type with many properties, this approach comes with the cost of explicit boilerplate to type. That’s not a problem for one with dogged determination. If short on time, there are other options such as dump
.
Dump
How to avoid needing to add boilerplate code manually? One solution is to use dump
. dump
is a generic function that prints out all the names and values of a type’s properties.
The playground already contains calls that dump out the net and Date
. The code looks like this:
dump(net) print() dump(Date()) print()
Run the playground. The console output looks something like:
▿ DogCatcherNet(Review Stars: two, Weight: 2.6) #0 - customerReviewStars: __lldb_expr_3.CustomerReviewStars.two - weightInPounds: 2.6 ▿ 2018-06-26 17:35:46 +0000 - timeIntervalSinceReferenceDate: 551727346.52924
Due to the work you’ve done so far with CustomDebugStringConvertible
, the DogCatcherNet
looks better than it otherwise would. The output contains:
DogCatcherNet(Review Stars: two, Weight: 2.6)
dump
also spits out each property automatically. Great! It’s time to make those properties more readable by using Swift’s Mirror
.
Swift Mirror
Mirror, mirror, on the wall. Who’s the fairest dog of them all?
Mirror
lets you display values of any type instance through the playground or the debugger at runtime. In short, Mirror
‘s power is introspection. Introspection is a subset of reflection
.
Creating a Mirror-Powered Dog Log
It’s time to create a Mirror-powered dog log. To help with debugging, it’s ideal to display the values of the net to the console through a log function with custom output complete with emoticons. The log function should be able to handle any item you pass it.
Creating a Mirror
It’s time to create a log function that uses a mirror. To start, add the following code right under ☆ Create log function here :
func log(itemToMirror: Any) { let mirror = Mirror(reflecting: itemToMirror) debugPrint("Type: :dog: \(type(of: itemToMirror)) :dog: ") }
This creates the mirror for the passed-in item. A mirror lets you iterate over the parts of an instance.
Add the following code to the end of the log(itemToMirror:)
:
for case let (label?, value) in mirror.children { debugPrint(":star: \(label): \(value) :star:") }
This accesses the children
property of the mirror, gets each label-value pair, then prints them out to the console. The label-value pair is type-aliased as Mirror.Child
. For a DogCatcherNet
instance, the code iterates over the properties of a net object.
To clarify, a child of the instance being inspected has nothing to do with a superclass or subclass hierarchy. The children accessible through a mirror are just the parts of the instance being inspected.
Now, it’s time to call your new log method. Add the following code right under ☆ Log out the net and a Date object here :
log(itemToMirror: net) log(itemToMirror: Date())
Run the playground. You’ll see at the bottom of the console output some doggone great output:
"Type: :dog: DogCatcherNet :dog: " ":star: customerReviewStars: two :star:" ":star: weightInPounds: 2.6 :star:" "Type: :dog: Date :dog: " ":star: timeIntervalSinceReferenceDate: 551150080.774974 :star:"
This shows all the properties’ names and values. The names appear as they do in your code. For example, customerReviewStars
is literally how the property name is spelled in code.
CustomReflectable
What if you wanted more of a dog and pony show in which the property names are displayed more clearly as well? What if you didn’t want some of the properties displayed? What if you wanted items displayed that are not technically part of the type? You’d use CustomReflectable
.
CustomReflectable
provides the hook with which you can specify what parts of a type instance are shown by using a custom Mirror
. To conform to CustomReflectable
, a type must define the customMirror
property.
After speaking with several dog catcher programmers, you’ve discovered that spitting out the weightInPounds
of the net has not helped with debugging. However, the customerReviewStars
information is extremely helpful and they’d like the label for customerReviewStars
to appear as “Customer Review Stars.” Now, it’s time to make DogCatcherNet
conform to CustomReflectable
.
Add the following code right under ☆ Add Conformance to CustomReflectable for DogCatcherNet here :
extension DogCatcherNet: CustomReflectable { public var customMirror: Mirror { return Mirror(DogCatcherNet.self, children: ["Customer Review Stars": customerReviewStars, ], displayStyle: .class, ancestorRepresentation: .generated) } }
Run the playground and see the following output:
"Type: :dog: DogCatcherNet :dog: " ":star: Customer Review Stars: two :star:"
Where’s the Dog?
The whole point of the net is to handle having a dog. When the net is populated with a dog, there must be a way to pull out information about the dog in the net. Specifically, you need the dog’s name and age.
The playground page already has a Dog
class. It’s time to connect Dog
with DogCatcherNet
. In the spot labeled as ☆ Add Optional called dog of type Dog here
, add the following property to DogCatcherNet
:
var dog: Dog?
With the dog property added to the DogCatcherNet
, it’s time to add the dog to the customMirror
for the DogCatcherNet
. Add the following dictionary entries right after the line children: ["Customer Review Stars": customerReviewStars,
:
"dog": dog ?? "", "Dog name": dog?.name ?? "No name"
This will output the dog using its default debug description and dog’s name, respectively labeled “dog” and “Dog name.”
Time to gently put a dog into the net. Right under ☆ Uncomment assigning the dog , uncomment that line so the cute little dog is put into the net:
net.dog = Dog() // ☆ Uncomment out assigning the dog
Run the playground and see the following:
"Type: :dog: DogCatcherNet :dog: " ":star: Customer Review Stars: two :star:" ":star: dog: __lldb_expr_23.Dog :star:" ":star: Dog name: Abby :star:"
Mirror Convenience
It’s pretty nice to be able to see everything. However, there are those times when you just want to pluck out a part from a mirror. To do that, you use
descendant(_:_:)
. Add the following code to the end of the playground page to create a mirror and use descendant(_:_:)
to pluck out the name and age:
let netMirror = Mirror(reflecting: net) print ("The dog in the net is \(netMirror.descendant("dog", "name") ?? "nonexistent")") print ("The age of the dog is \(netMirror.descendant("dog", "age") ?? "nonexistent")")
Run the playground and see at the bottom of the console output:
The dog in the net is Bernie The age of the dog is 2
That’s doggone dynamic introspection there. It can be quite useful for debugging your own types! Having deeply explored Mirror
, you’re done with DogMirror.xcplaygroundpage
.
Wrapping Up Mirror and Debug Output
There are many ways to track, like a bloodhound, what’s going on in a program. CustomDebugStringConvertible
, dump
and Mirror
let you see more clearly what you are hunting for. Swift’s introspection power is highly useful — especially as you start building bigger and more complex applications!
Key Paths
On the subject of tracking what’s going on in a program, Swift has something wonderful called key paths. For capturing an event such as when a value has changed in a third-party library object, look to KeyPath
‘s observe
for help.
In Swift, key paths are strongly typed paths whose types are checked at compile time. In Objective-C, they were only strings. The tutorial What’s New in Swift 4? does a great job covering the concepts in the Key-Value Coding section.
There are several different types of KeyPath
. Commonly discussed types include KeyPath
, WritableKeyPath
and ReferenceWritableKeyPath
. Here’s a summary of the different ones:
KeyPath WritableKeyPath ReferenceWritableKeyPath
A practical example of using a key path is in observing or capturing when a value changes on an object.
When you encounter a bug involving a third-party object, there is immense power in knowing when the state of that object changes. Beyond debugging, sometimes it just makes sense to hook up your custom code to respond when a value changes in a third-party object such as Apple’s UIImageView object. In Design Patterns on iOS using Swift – Part 2/2 , you can learn more about the observer pattern in the section titled Key-Value Observing (KVO) .
However, there’s a use case here related to the kennels that fits right into our doggy world. Without this KVO power, how would dog catchers easily know when the kennels are available to put more dogs inside? Although many dog catchers would just love to take home each and every lost dog they find, it’s just not practical.
So dog catchers who just want to help dogs find their way home need to know when the kennels are available to place dogs into. The first step to making this possible is creating a key path. Open the KennelsKeyPath page and, right after ☆ Add KeyPath here , add this:
let keyPath = \Kennels.available
This is how you create a KeyPath
. You use a backslash on the type, followed by a chain of dot-separated properties — in this case, one property deep. To use the KeyPath
to observe changes to the available
property, add the following code after ☆ Add observe method call here
:
kennels.observe(keyPath) { kennels, change in if (kennels.available) { print("kennels are available") } }
Click run and see the following message output to the console:
Kennels are available.
This approach can also be great for figuring out when a value has changed. Imagine being able to debug state changes to third-party objects! Nailing down when an item of interest changes can really keep you from barking up the wrong tree.
You’re done with the KennelsKeyPath page!
Understanding Dynamic Member Lookup
If you’ve been keeping up with Swift 4.2 changes, you may have heard about Dynamic Member Lookup . If not, you’ll go beyond just learning the concept here.
In this part of the tutorial, you’ll see the power of Dynamic Member Lookup in Swift by going over an example of how to create a real JSON DSL (Domain Specification Language) that allows the caller to use dot notation to access values from a JSON dictionary.
Dynamic Member Lookup empowers the coder to use dot syntax for properties that don’t exist at compile time as opposed to messier ways. In short, you’re coding on faith that the members will exist at runtime and getting nice-to-read code in the process.
As mentioned in the proposal for this feature and associated conversations in the Swift community , this power offers great support for interoperability with other languages such as Python, database implementors and creating boilerplate-free wrappers around “stringly-typed” APIs such as CoreImage.
Introducing @dynamicMemberLookup
Open the DogCatcher
page and review the code. In the playground, Dog
represents the way a dog is running with Direction
.
With the dynamicMemberLookup
power, directionOfMovement
and moving
can be accessed even though those properties don’t explicitly exist. It’s time to make Dog
dynamic.
Adding dynamicMemberLookup to the Dog
The way to activate this dynamic power is through the use of the type attribute @dynamicMemberLookup
.
Add the following code under ☆ Add subscript method that returns a Direction here :
subscript(dynamicMember member: String) -> Direction { if member == "moving" || member == "directionOfMovement" { // Here's where you would call the motion detection library // that's in another programming language such as Python return randomDirection() } return .motionless }
Now add dynamicMemberLookup
to Dog
by uncommenting the line that’s marked ☆ Uncomment this line
above Dog
.
You can now access a property named directionOfMovement
or moving
. Give it a try by adding the following on the line after ☆ Use the dynamicMemberLookup feature for dynamicDog here
:
let directionOfMove: Dog.Direction = dynamicDog.directionOfMovement print("Dog's direction of movement is \(directionOfMove).") let movingDirection: Dog.Direction = dynamicDog.moving print("Dog is moving \(movingDirection).")
Run the playground. With the values sometimes being left and sometimes being right , the first two lines you should see are similar to:
Dog's direction of movement is left. Dog is moving left.
Overloading subscript(dynamicMember:)
Swift supports overloading subscript declarations
with different return types. Try this out by adding a subscript
that returns an Int
right under ☆ Add subscript method that returns an Int here
:
subscript(dynamicMember member: String) -> Int { if member == "speed" { // Here's where you would call the motion detection library // that's in another programming language such as Python. return 12 } return 0 }
Now, you can access a property named speed
. Speed to victory by adding the following under movingDirection
that you added earlier:
let speed: Int = dynamicDog.speed print("Dog's speed is \(speed).")
Run the playground. The output should contain:
Dog's speed is 12.
Pretty nice, huh? That’s a powerful feature that keeps the code looking nice even if you need to access other programming languages such as Python. As hinted at earlier, there’s a catch…
“A catch?” I’m all ears.
Compiler and Code Completion Gone to the Dogs
In exchange for this dynamic runtime feature, you don’t get the benefits of compile-time checking of properties that depend on the subscript(dynamicMember:)
functionality. Also, Xcode’s code completion feature can’t help you out either. However, the good news is that professional iOS developers read more code than they write.
The syntactic sugar that Dynamic Member Lookup gives you is nothing to just throw away. It’s a nice feature that makes certain specific use cases of Swift and language interoperability bearable and enjoyable to view.
Friendly Dog Catcher
The original proposal for Dynamic Member Lookup addressed language interoperability, particularly with Python. However, that’s not the only circumstance where it’s useful.
To demonstrate a pure Swift use case, you’re going to work on the JSONDogCatcher
code found in DogCatcher.xcplaygroundpage
. It’s a simple struct with a few properties designed to handle String
, Int
and JSON dictionary. With a struct like this, you can create a JSONDogCatcher
and ultimately go foraging for specific String
or Int
values.
Traditional Subscript Method
A traditional way of drilling down into a JSON dictionary with a struct like this is to use a subscript
method. The playground already contains a traditional subscript
implementation. Accessing the String
or Int
values using the subscript
method typically looks like the following and is also in the playground:
let json: [String: Any] = ["name": "Rover", "speed": 12, "owner": ["name": "Ms. Simpson", "age": 36]] let catcher = JSONDogCatcher.init(dictionary: json) let messyName: String = catcher["owner"]?["name"]?.value() ?? "" print("Owner's name extracted in a less readable way is \(messyName).")
Although you have to look past the brackets, quotes and question marks, this works.
Run the playground. You can now see the following:
Owner's name extracted in a less readable way is Ms. Simpson.
Although it works fine, it would be easier on the eyes to just use dot syntax. With Dynamic Member Lookup , you can drill down a multi-level JSON data structure.
Adding dynamicMemberLookup to the Dog Catcher
Like Dog
, it’s time to add the dynamicMemberLookup
attribute to the JSONDogCatcher
struct.
Add the following code right under ☆ Add subscript(dynamicMember:) method that returns a JSONDogCatcher here :
subscript(dynamicMember member: String) -> JSONDogCatcher? { return self[member] }
The subscript(dynamicMember:)
method calls the already existing subscript
method but takes away the boilerplate code of using brackets and String
keys. Now, uncomment the line which has ☆ Uncomment this line
above JSONDogCatcher
:
@dynamicMemberLookup struct JSONDogCatcher {
With that in place, you can use dot notation to get the dog’s speed and owner’s name. Try it out by adding the following right under ☆ Use dot notation to get the owner’s name and speed through the catcher :
let ownerName: String = catcher.owner?.name?.value() ?? "" print("Owner's name is \(ownerName).") let dogSpeed: Int = catcher.speed?.value() ?? 0 print("Dog's speed is \(dogSpeed).")
Run the playground. See the speed and the owner’s name in the console:
Owner's name is Ms. Simpson. Dog's speed is 12.
Now that you have the owner’s name, the dog catcher can contact the owner and let her know her dog has been found!
What a happy ending! The dog and its owner are together again and the code looks cleaner. Through the power of dynamic Swift, this dynamic dog can go back to chasing bunnies in the backyard.
Simpson’s dog loves chasing but not catching
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you harnessed the dynamic power that Swift offers in version 4.2. You learned about Swift’s introspective reflection powers such as Mirror
, customizing console output, hooking into Key-Value Observing with KeyPaths
and Dynamic Member Lookup
.
With that dynamic smorgasbord, you can clearly see helpful information, have more readable code and hook into some powerful runtime capabilities for your app or general purpose framework and library.
Sniffing around Apple’s documentation about Mirror and related items is a worthy endeavor. For more about Key-Value Observing , have a look at Design Patterns on iOS using Swift . Be sure to check out the What’s New in Swift 4.2? to see more of what’s in Swift 4.2.
In regards to the Dynamic Member Lookup
Swift 4.2 feature, it doesn’t hurt to look at the Swift proposal SE-0195: “Introduce User-defined ‘Dynamic Member Lookup’ Types”
that introduces the dynamicMemberLookup
type attribute and potential use cases. On a related note, a Swift proposal to keep an eye on is the close cousin of Dynamic Member Lookup
called SE-216: “Introduce User-defined Dynamically ‘callable’ Types
that introduces the dynamicCallable
type attribute.
Have more to say or ask about the dynamic power of Swift? Join in on the forum discussion below!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK