Section 8/101 menit
8. Real Use Cases di Xcode Project
8. Real Use Cases di Xcode Project
Use Case 1: @Singleton Macro untuk Dependency Injection
Konteks: Di project iOS, banyak service class yang perlu pola singleton. Menulis static let shared = Self() berulang-ulang membosankan dan rawan lupa.
Implementasi Macro:
swift
// Public API — MyMacros/Sources/MyMacros/MyMacros.swift
@attached(member, names: named(shared))
public macro Singleton() = #externalMacro(
module: "MyMacrosMacros",
type: "SingletonMacro"
)
swift
// Implementasi — MyMacrosMacros/Sources/MyMacrosMacros/SingletonMacro.swift
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct SingletonMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Validasi: hanya boleh pada class
guard declaration.is(ClassDeclSyntax.self) else {
throw MacroError.requiresClass("@Singleton hanya bisa digunakan pada class")
}
guard let typeName = declaration.asProtocol(NamedDeclSyntax.self)?.name.text else {
throw MacroError.unknownType
}
return [
"""
public static let shared = \(raw: typeName)()
""",
"""
private override init() {
super.init()
}
"""
]
}
}
Penggunaan di Xcode Project:
swift
import MyMacros
// ✓ Bersih, tidak ada boilerplate
@Singleton
class NetworkManager: NSObject {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
// implementasi
}
}
@Singleton
class UserSessionManager: NSObject {
private(set) var currentUser: User?
func login(with credentials: Credentials) async throws {
// implementasi
}
func logout() {
currentUser = nil
}
}
// Penggunaan:
class HomeViewController: UIViewController {
private let network = NetworkManager.shared
private let session = UserSessionManager.shared
override func viewDidLoad() {
super.viewDidLoad()
Task {
let user = UserSessionManager.shared.currentUser
// ...
}
}
}
Test:
swift
func testSingletonMacroExpansion() {
assertMacroExpansion(
"""
@Singleton
class MyService: NSObject {}
""",
expandedSource: """
class MyService: NSObject {
public static let shared = MyService()
private override init() {
super.init()
}
}
""",
macros: testMacros
)
}
Use Case 2: #URL Expression Macro untuk Type-Safe URL
Konteks: URL yang salah format baru ketahuan saat runtime dan menyebabkan crash. Dengan macro, URL divalidasi saat compile time.
Implementasi Macro:
swift
// Public API
@freestanding(expression)
public macro URL(_ string: String) -> Foundation.URL = #externalMacro(
module: "MyMacrosMacros",
type: "URLMacro"
)
swift
// Implementasi
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import Foundation
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// Ambil argumen string literal
guard
let argument = node.arguments.first?.expression,
let stringLiteral = argument.as(StringLiteralExprSyntax.self),
let urlString = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text
else {
throw URLMacroError.requiresStringLiteral
}
// Validasi URL di compile time
guard let _ = Foundation.URL(string: urlString), !urlString.isEmpty else {
throw URLMacroError.invalidURL(urlString)
}
// Pastikan URL memiliki scheme yang valid
guard urlString.hasPrefix("https://") || urlString.hasPrefix("http://") else {
throw URLMacroError.missingScheme(urlString)
}
// Generate ekspresi URL yang guaranteed valid
return "Foundation.URL(string: \(literal: urlString))!"
}
}
enum URLMacroError: Error, CustomStringConvertible {
case requiresStringLiteral
case invalidURL(String)
case missingScheme(String)
var description: String {
switch self {
case .requiresStringLiteral:
return "#URL membutuhkan string literal, bukan ekspresi dinamis"
case .invalidURL(let url):
return "'\(url)' bukan URL yang valid"
case .missingScheme(let url):
return "'\(url)' harus memiliki scheme https:// atau http://"
}
}
}
Penggunaan di Xcode Project:
swift
import MyMacros
import Foundation
enum APIEndpoint {
static let base = #URL("https://api.example.com/v1") // ✓ valid
static let users = #URL("https://api.example.com/v1/users") // ✓ valid
static let broken = #URL("not-a-url") // ❌ COMPILE ERROR!
static let noScheme = #URL("api.example.com/users") // ❌ COMPILE ERROR!
}
// Penggunaan di NetworkManager:
class NetworkManager {
private let session = URLSession.shared
func fetchUsers() async throws -> [User] {
let url = APIEndpoint.users
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode([User].self, from: data)
}
}
// Penggunaan di VIP Interactor:
class HomeInteractor: HomeBusinessLogic {
private let network = NetworkManager()
func fetchHomeData(request: Home.FetchData.Request) async {
do {
let url = #URL("https://api.example.com/v1/home")
// url dijamin valid — tidak perlu optional unwrapping
let (data, _) = try await URLSession.shared.data(from: url)
// proses data...
} catch {
// handle network error
}
}
}
Use Case 3: @Logged Peer Macro untuk Automatic Logging
Konteks: Di project production, logging penting untuk debugging. Tapi menambahkan logging manual ke setiap fungsi membutuhkan banyak kode berulang dan mudah terlupa.
Implementasi Macro:
swift
// Public API
@attached(peer, names: prefixed(__logged_))
public macro Logged() = #externalMacro(
module: "MyMacrosMacros",
type: "LoggedMacro"
)
swift
// Implementasi
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct LoggedMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw LoggedMacroError.onlyFunctions
}
let funcName = funcDecl.name.text
let params = funcDecl.signature.parameterClause.parameters
let returnType = funcDecl.signature.returnClause?.type.description ?? "Void"
let isAsync = funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil
let isThrowing = funcDecl.signature.effectSpecifiers?.throwsClause != nil
// Buat parameter call string untuk forward ke fungsi asli
let paramCall = params.map { param in
let label = param.firstName.text
let name = param.secondName?.text ?? param.firstName.text
if label == "_" {
return name
}
return "\(label): \(name)"
}.joined(separator: ", ")
// Buat daftar nama parameter untuk logging
let paramLog = params.map { param in
let name = param.secondName?.text ?? param.firstName.text
return "\\(\(name))"
}.joined(separator: ", ")
let asyncKeyword = isAsync ? "async " : ""
let throwsKeyword = isThrowing ? "throws " : ""
let awaitKeyword = isAsync ? "await " : ""
let tryKeyword = isThrowing ? "try " : ""
let loggedFuncName = "__logged_\(funcName)"
return [
"""
func \(raw: loggedFuncName)(\(raw: params.description)) \(raw: asyncKeyword)\(raw: throwsKeyword)-> \(raw: returnType) {
let startTime = Date()
os_log(.debug, "[\(raw: funcName)] Called with: \(raw: paramLog.isEmpty ? "(no params)" : paramLog)")
defer {
let elapsed = Date().timeIntervalSince(startTime)
os_log(.debug, "[\(raw: funcName)] Completed in %.3f seconds", elapsed)
}
return \(raw: tryKeyword)\(raw: awaitKeyword)\(raw: funcName)(\(raw: paramCall))
}
"""
]
}
}
enum LoggedMacroError: Error {
case onlyFunctions
}
Penggunaan di Xcode Project (VIP Architecture):
swift
import MyMacros
import os
// Interactor dengan logging otomatis
class ProfileInteractor: ProfileBusinessLogic {
var presenter: ProfilePresentationLogic?
private let worker = ProfileWorker()
@Logged
func fetchProfile(request: Profile.FetchProfile.Request) async {
do {
let user = try await worker.getUser(id: request.userId)
let response = Profile.FetchProfile.Response(user: user)
await presenter?.presentProfile(response: response)
} catch {
let response = Profile.FetchProfile.Response(error: error)
await presenter?.presentProfile(response: response)
}
}
@Logged
func updateProfile(request: Profile.UpdateProfile.Request) async throws {
let updated = try await worker.updateUser(
id: request.userId,
name: request.name,
email: request.email
)
let response = Profile.UpdateProfile.Response(user: updated)
await presenter?.presentUpdateResult(response: response)
}
}
// Macro generate peer function untuk setiap @Logged function:
// func __logged_fetchProfile(request: Profile.FetchProfile.Request) async {
// let startTime = Date()
// os_log(.debug, "[fetchProfile] Called with: ...")
// defer { os_log(.debug, "[fetchProfile] Completed in X.XXX seconds", ...) }
// return await fetchProfile(request: request)
// }
// ViewController menggunakan __logged_ variant untuk observability:
class ProfileViewController: UIViewController {
var interactor: ProfileBusinessLogic?
override func viewDidLoad() {
super.viewDidLoad()
Task {
// Dalam production mode, gunakan __logged_ variant
if let loggedInteractor = interactor as? ProfileInteractor {
await loggedInteractor.__logged_fetchProfile(
request: .init(userId: "user_123")
)
}
}
}
}
Test untuk @Logged Macro:
swift
func testLoggedMacroGeneratesPeer() {
assertMacroExpansion(
"""
@Logged
func greet(name: String) -> String {
return "Hello, \\(name)"
}
""",
expandedSource: """
func greet(name: String) -> String {
return "Hello, \\(name)"
}
func __logged_greet(name: String) -> String {
let startTime = Date()
os_log(.debug, "[greet] Called with: \\(name)")
defer {
let elapsed = Date().timeIntervalSince(startTime)
os_log(.debug, "[greet] Completed in %.3f seconds", elapsed)
}
return greet(name: name)
}
""",
macros: testMacros
)
}