I am not sure when Playground was introduced to Xcode, may version 2? I started to use Playground when I moved to Xcode 3. As I mentioned in my previous blog, I would like to find out a better way to test Subquery
.
It is very easy to add a Playground in iOS project in Xcode. However, it is hard to test CoreData
in Playground
. First, CoreData model is not easy to add to Playground, even though I eventually found a way to add CoreData
context to Playground
. Next difficulty is that I still cannot fig out how to add CoreData
managed data classes to Playground
. It seems that namespace between my project and Playground are different. I could change all my managed classes to public, but they are not visible nor accessible through CoreData
fetch mechanism.
My purpose is to test Subquery
. When I found out that Subquery
can be built in NSExpression
, I immediately think about Playground as a place to do my test.
Playground in Xcode
To add a Playground to Xcode project is very simple: just add a new file to Xcode project and then select Playground. By default, it is called as MyPlayground. There are two folders within the Playground: Sources for all swift source codes, and Resources for resource files.
The following is MyPlayground in Xcode left panel Project navigator view.
On the right panel, the left area on the top is an editable area for swift codes. The right side on the top is an area for immediate result view and the bottom is debug result view.
It is better to organize all the classes, extensions, enumerations and types in swift files under Sources folder. In order to access them from
Playground
, all the accessible elements have to be public.During your start test stage, you may need to place all your classes and variables, types, and functions in
Playground
. In this way, any run-time errors would be highlighted during your test. When all those elements are OK, you can move them to a swift file under Sources folder.Test Class
As I mentioned in my previous blog, the first parameter in
Subquery
is a collection of objects, defined by a class. What I found that there are some requirement for the class. First, the class has to inherit from NSObject
which has hash code for equality. Secondly, if you need to use operator ALL
, ANY
, or NONE
on collection properties defined in the class, the collection has to be NSSet
type.For my test, I defined the following
Person
class.- public class Person : NSObject {
- public var name: String
- public let birthYear: Int
- public var friends: NSSet?
- init(name pName: String, birthYear year : Int) {
- name = pName
- birthYear = year
- super.init()
- }
- }
I created a swift file TestSubquery.swift in Sources folder. The
Person
class is defined there. This class is perfect data for Subquery
's first parameter: collection. The class has two simple properties name
and birthYear
, and one collection property friends
. In my test, only one person object is created and placed in an array as collection, which is used as the first parameter in Subquery
.friends
property is optional NSSet
type collection, where actually Person
objects are hold or null if no objects. Each person in friends
could have friends
. In this way, nested friends
could be used as many levels of one-to-many relationship for testing. The following is my help function(in TestSubquery.swift file as a module function) to create base data for my tests. The function takes one parameter to control levels of nested friends. I'll explain how to use this parameter to do some interesting tests later.- func getPerson(friendsLevel: Int) -> [Person] {
- print("Creating Person object and friends...")
- let p = Person(name: "Bob", birthYear: 1997)
- if friendsLevel > 0 {
- let p1 = Person(name: "Tony", birthYear: 2010)
- if friendsLevel > 1 {
- let p1_1 = Person(name: "Tony1", birthYear: 2012)
- if friendsLevel > 2 {
- let p1_1_1 = Person(name: "Tony1_1", birthYear: 2011)
- let p1_1_2 = Person(name: "Tony1_2", birthYear: 2012)
- p1_1.friends = NSSet(objects: p1_1_1, p1_1_2)
- }
- let p1_2 = Person(name: "Lisa1", birthYear: 2011)
- p1.friends = NSSet(objects: p1_1, p1_2)
- }
- let p2 = Person(name: "Lisa", birthYear: 2012)
- p.friends = NSSet(objects: p1, p2)
- }
- printPerson(personCollection: [p], printFriends: true)
- return [p]
- }
Here is the complete debug information of person data structure (up to 2 levels of nested friends):
- Creating Person object and friends...
- Person - name: Bob, birthYear: 1997
- Friend - name: Tony, birthYear: 2010
- Friend - name: Tony1, birthYear: 2012
- Friend - name: Tony1_2, birthYear: 2012, friends: (none)
- Friend - name: Tony1_1, birthYear: 2011, friends: (none)
- Friend - name: Lisa1, birthYear: 2011, friends: (none)
- Friend - name: Lisa, birthYear: 2012, friends: (none)
In addition to that, I have added some other types and help functions, as well as a test class
TestSubquery
. Here I don't need to list all the codes for them. Only test codes will be provided and explained in the following sections.The complete source code can be downloaded from a link in Resources section.
Test Structure
Subquery NSExpression
will be used as an expression object for my tests. To build a Subquery
expression, two things have to be prepared: a collection data or expression and a predicate. Therefore, all my tests are defined in the following structure or steps:- get an expression of
Person
collection, - create a predicate, and
- build a
Subquery
expression, finally evaluate theSubquery
expression to get result.
Step 1 is very simple. The collection of
Person
is obtained from the function getPerson(...)
as explained above. In step 2, a predicate is created by one of a list of help functions, which define interested fetch query conditions against to the collection of Person
. Finally, it is time ready to build a Subquery
expression and then to evaluate its result in step 3.Let's look at a simple test
func
defined in TestSubquery class.- public class TestSubquery {
- ...
- public static func TestSubquery(nameContains: String, birthYearLowLimit: Int, friendsLevel: Int = 2) {
- let e = getColletionExpression(friendsLevel: friendsLevel)
- let predicate = getPredicate(nameContains: nameContains, birthYearLowLimit: birthYearLowLimit)
- let expValue = getSubqueryExpressionResult(collection: e, predicate: predicate)
- printSubqueryExpressionWith(result: expValue)
- }
- ...
- }
At line 4, only one person with a name, birth year and a collection of
friends
(or null if no friends) is created as collection expression by the help func
of getCollectionExpression
. At line 5, a predicate with CONTAINS(string)
function for name and low limit for birth year is obtained from the help func
of getPredicate
. With the data collection and predicate ready, at line 6, the help func
of getSubqueryExpressionResult
builds a Subquery
expression and evaluates the result.Test and Result
To better understand my test, extensive information are printed out in debug window. Here is one example of using above test
func
in Playground
:
TestSubquery.TestSubquery(nameContains: "L", birthYearLowLimit: 2010, friendsLevel: 0)
and the debug information is as follows:
- Creating Person object and friends...
- Person - name: Bob, birthYear: 1997
- Collection expression: {<MyPlayground_Sources.Person: 0x60800007e940>}
- Collection expression result value: (
- "<MyPlayground_Sources.Person: 0x60800007e940>"
- )
- Person - name: Bob, birthYear: 1997
- Predicate: $x.name CONTAINS "L" AND $x.birthYear > 2010
- Subquery Expression: SUBQUERY({<MyPlayground_Sources.Person: 0x60800007e940>}, $x, $x.name CONTAINS "L" AND $x.birthYear > 2010)
- collection: {<MyPlayground_Sources.Person: 0x60800007e940>}
- variable: x
- predicate: $x.name CONTAINS "L" AND $x.birthYear > 2010
- Subquery expression result value: Optional(<__NSArrayM 0x610000046030>(
- )
- )
- Subquery expression value as [Person]: [] as follows
In this test result, line 1-7 explain that only one person "Bob" who is born in 1997 created as in a collection expression. The debug information at line 8 is the print-out during a predicate was obtained. From line 9-12 are information about building
Subquery
expression, and its three parameters: collection, variable and predicate. The information at remaining lines shows the Subquery
expression evaluation result, and detail information as an array of Person
or empty if no result.If test is changed to name containing "B" and birth year greater than 1990, the person Bob will be in the result array:
- Creating Person object and friends...
- Person - name: Bob, birthYear: 1997
- Collection expression: {<MyPlayground_Sources.Person: 0x608000077fc0>}
- Collection expression result value: (
- "<MyPlayground_Sources.Person: 0x608000077fc0>"
- )
- Person - name: Bob, birthYear: 1997
- Predicate: $x.name CONTAINS "B" AND $x.birthYear > 1990
- Subquery Expression: SUBQUERY({<MyPlayground_Sources.Person: 0x608000077fc0>}, $x, $x.name CONTAINS "B" AND $x.birthYear > 1990)
- collection: {<MyPlayground_Sources.Person: 0x608000077fc0>}
- variable: x
- predicate: $x.name CONTAINS "B" AND $x.birthYear > 1990
- Subquery expression result value: Optional(<__NSArrayM 0x60800005cc50>(
- <MyPlayground_Sources.Person: 0x608000077fc0>
- )
- )
- Subquery expression value as [Person]: [<MyPlayground_Sources.Person: 0x608000077fc0>] as follows
- Person - name: Bob, birthYear: 1997
It is really fun to test
Subquery
expression in Playground
.Stay tuned for the power of
Subquery
expression and more interesting findings in my other tests.Resources
- Article by Andrew Bancroft: Using a Core Data Model in Swift Playground
- StackOverflow QA: NSExpression Subquery usage
- Apple's Developer API Reference: NSExpression
- Apple's Developer API Reference: Creating Subquery NSExpression init(...)
- Previous blog: One-to-Many Relationship and Subquery
- Next blog: Playground and Testing Subquery by NSExpression (2)
- Source Code: TestSubquery.swift
0 comments:
Post a Comment