LePips / SwiftAndTipsMacros

A list of Swift Macros to make your coding live on Apple ecosystem simpler and more productive.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SwiftAndTipsMacros

This repository contains a list of Swift Macros to make your coding live on Apple ecosystem simpler and more productive.

Requirements

  • Xcode 15 or above.
  • Swift 5.9 or above.
  • Platforms:
    • macOS 10.15 or above.
    • iOS 13.0 or above.
    • tvOS 13.0 or above.
    • watchOS 6.0 or above.
    • macCatalyst 13.0 or above.

Macros

#binaryString

#binaryString is a freestanding macro that will convert an Integer literal into a binary string representation:

let x = #binaryString(10)
/*
expanded code:
"1010"
*/
print(x) // Output: "1010"

This macro was created as a tutorial to explain how macros work. It would be simpler to create a function to do this instead :). Learn more here: TBD

@SampleBuilder

The aim of @SampleBuilder is straightforward: Generate an array of sample data from your models for use in SwiftUI previews, unit tests, or any scenario that needs mock data—without the hassle of crafting it from scratch.

Interested in a demonstration? Check out this video

How to use it?

  1. Import SwiftAndTipsMacros and DataGenerator.
  2. Attach @SampleBuilder to an struct or enum.
  3. Provide the number of items you want for your sample.
  4. Provide the type of data generator you want to use:
    • default will generate a fixed value all the time (ideal for unit tests).
    • random will generate a random value for each property requested in the initialization.

In this example, we are using the default generator to generate 10 items:

// 1
import SwiftAndTipsMacros
import DataGenerator
// 2
@SampleBuilder(
    numberOfItems: 10, // 3
    dataGeneratorType: .default // 4
)
struct Example {
    let item1: String
    let item2: Int
    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
            .init(item1: DataGenerator.default.string(), item2: DataGenerator.default.int()),
        ]
    }
    #endif
    */
}
...
for element in Example.sample {
    print(element.item1, element.item2)
}
/*
Output:
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
Hello World 0
*/

To optimize your production code, the sample property is available only in DEBUG mode. Ensure you use the #if DEBUG condition or any other custom flag specific to debug mode before archiving your app.

Now, if you need a more realistic data, you can use random generator type:

@SampleBuilder(numberOfItems: 10, dataGeneratorType: .random)
struct Example {
    let item1: String
    let item2: Int
    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
            .init(item1: DataGenerator.random().string(), item2: DataGenerator.random().int()),
        ]
    }
    #endif
    */
}
...
for element in Example.sample {
    print(element.item1, element.item2)
}
/*
Output:
1234-2121-1221-1211 738
6760 Nils Mall Suite 390, Kesslerstad, WV 53577-7421 192
yazminzemlak1251 913
lelahdaugherty 219
Tony 228
Jessie 826
alanvonrueden6307@example.com 864
Enola 858
Fay 736
myrtismcdermott@example.net 859
*/

Supported Foundation Types

The current supported list includes:

  • UUID
  • Array*
  • Dictionary*
  • Optional*
  • String
  • Int
  • Bool
  • Data
  • Date
  • Double
  • Float
  • Int8
  • Int16
  • Int32
  • Int64
  • UInt8
  • UInt16
  • UInt32
  • UInt64
  • URL
  • CGPoint
  • CGFloat
  • CGRect
  • CGSize
  • CGVector

* It includes nested types too!

More types will be supported soon.

.default Generator Values

Type Value
UUID 00000000-0000-0000-0000-000000000000 (auto increasing)
String "Hello World"
Int 0
Bool true
Data Data()
Date Date(timeIntervalSinceReferenceDate: 0)
Double 0.0
Float 0.0
Int8 0
Int16 0
Int32 0
Int64 0
UInt8 0
UInt16 0
UInt32 0
UInt64 0
URL URL(string: "https://www.apple.com")!
CGPoint CGPoint()
CGFloat CGFloat()
CGRect CGRect()
CGSize CGSize()
CGVector CGVector()

Custom Types

You can add @SampleBuilder to all your custom types to generate sample data from those types. Here's an example:

@SampleBuilder(numberOfItems: 3, dataGeneratorType: .random)
struct Review {
    let rating: Int
    let time: Date
    let product: Product
    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .init(rating: DataGenerator.random().int(), time: DataGenerator.random().date(), product: Product.sample.first!),
            .init(rating: DataGenerator.random().int(), time: DataGenerator.random().date(), product: Product.sample.first!),
            .init(rating: DataGenerator.random().int(), time: DataGenerator.random().date(), product: Product.sample.first!),
        ]
    }
    #endif
    */
}

@SampleBuilder(numberOfItems: 3, dataGeneratorType: .random)
struct Product {
    var price: Int
    var description: String
    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .init(price: DataGenerator.random().int(), description: DataGenerator.random().string()),
            .init(price: DataGenerator.random().int(), description: DataGenerator.random().string()),
            .init(price: DataGenerator.random().int(), description: DataGenerator.random().string()),
        ]
    }
    #endif
    */
}

To generate the sample property in structs, we always take the initialize with the longest number of parameters available. If there are no initializers available, we use the memberwise init.

Enums

Enums are also supported by @SampleBuilder.

@SampleBuilder(numberOfItems: 6, dataGeneratorType: .random)
enum MyEnum {
    indirect case case1(String, Int, String, [String])
    case case2
    case case3(Product)
    case case4([String: Product])

    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .case1(DataGenerator.random().string(), DataGenerator.random().int(), DataGenerator.random().string(), [DataGenerator.random().string()]),
            .case2,
            .case3(Product.sample.first!),
            .case4([DataGenerator.random().string(): Product.sample.first!]),
            .case1(DataGenerator.random().string(), DataGenerator.random().int(), DataGenerator.random().string(), [DataGenerator.random().string()]),
            .case2,
        ]
    }
    #endif
    */
}

To generate the sample for enums, we are adding each case to sample array one by one and starting over if numberOfItems is larger than the number of cases.

@SampleBuilderItem

If you want to customize your sample data even further for .random generator, you can use @SampleBuilderItem to specify the type of data you want to generate.

The following list shows the supported categories:

  • String:
    • firstName
    • lastName
    • fullName
    • email
    • address
    • appVersion
    • creditCardNumber
    • companyName
    • username
  • Double:
    • price
  • URL:
    • url (generic web link)
    • image (image url)

More category will be added soon.

Here's an example:

@SampleBuilder(numberOfItems: 3, dataGeneratorType: .random)
struct Profile {
    @SampleBuilderItem(category: .firstName)
    let firstName: String
    
    @SampleBuilderItem(category: .lastName)
    let lastName: String
    
    @SampleBuilderItem(category: .image(width: 300, height: 300))
    let profileImage: URL
    /*
    expanded code:
    #if DEBUG
    static var sample: [Self] {
        [
            .init(firstName: DataGenerator.random(dataCategory: .init(rawValue: "firstName")).string(), lastName: DataGenerator.random(dataCategory: .init(rawValue: "lastName")).string(), profileImage: DataGenerator.random(dataCategory: .init(rawValue: "image(width:300,height:300)")).url()),
            .init(firstName: DataGenerator.random(dataCategory: .init(rawValue: "firstName")).string(), lastName: DataGenerator.random(dataCategory: .init(rawValue: "lastName")).string(), profileImage: DataGenerator.random(dataCategory: .init(rawValue: "image(width:300,height:300)")).url()),
            .init(firstName: DataGenerator.random(dataCategory: .init(rawValue: "firstName")).string(), lastName: DataGenerator.random(dataCategory: .init(rawValue: "lastName")).string(), profileImage: DataGenerator.random(dataCategory: .init(rawValue: "image(width:300,height:300)")).url()),
        ]
    }
    #endif
    */
}

/*
Output: 
Sylvia Ullrich https://picsum.photos/300/300
Precious Schneider https://picsum.photos/300/300
Nyasia Tromp https://picsum.photos/300/300
*/

@SampleBuilderItem only works with random generator in structs. If you use this macro within default generator, a warning will appear indicating that macro is redundand.

Installation

import PackageDescription

let package = Package(
  name: "<TARGET_NAME>",
  dependencies: [
  	// ...
    .package(url: "https://github.com/pitt500/SwiftAndTipsMacros.git", branch: "main")
  	// ...
  ]
)

Limitations

@SampleBuilder

  • Conflict with #Preview and expanded sample property: For some reason, if you call sample property directly within a #Preview macro, the project will not compile.
#Preview {
    ContentView(people: Person.sample) 
    //Error: Type 'Person' has no member 'sample'
}

Workaround: Just create an instance that holds the view and use it inside #Preview instead of directly calling the View and sample:

#Preview {
    contentView
}

let contentView = ContentView(people: Person.sample)
  • Both SwiftAndTipsMacros and DataGenerator are required to be imported in order to make @SampleBuilder work. I've explored another alternative using @_exported that will reimport DataGenerator directly from SwiftAndTipsMacros, allowing you to just requiring one import, however, using underscored attributes is not recommended because it may break your code after a new Swift release.

If you want more information about @_exported, watch this video.

Future Work

  • Create documentation to all functions, structs and enums needed and export it usind DocC.

@SampleBuilder

  • Adding support to CGPoint and more types in random generator mode.
  • Remove the importing of DataGeneration once @_exported can be used publicly.
  • Adding more macros useful for your development.

Contributing

There are a lot of work to do, if you want to contribute adding a new macro or fixing an existing one, feel free to fork this project and follow these rules before creating a PR:

  1. Include unit tests in your PR (unless is just to fix a typo).
  2. Please add a description in your PR with the purpose of your change or new macro.
  3. Add the following header to all your code files:
/*
 This source file is part of SwiftAndTipsMacros

 Copyright (c) 2023 Pedro Rojas and project authors
 Licensed under MIT License
*/

Contact

If you have any feedback, I would love to hear from you. Please feel free to reach out to me through any of my social media channels:

Thanks you, and have a great day! 😄

License

Licensed under MIT License, see LICENSE for more information.

About

A list of Swift Macros to make your coding live on Apple ecosystem simpler and more productive.

License:MIT License


Languages

Language:Swift 100.0%