관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] ResultBuilder, @resultBuilder (빌더, return 없이 사용 방법) 본문

iOS 응용 (swift)

[iOS - swift] ResultBuilder, @resultBuilder (빌더, return 없이 사용 방법)

jake-kim 2021. 12. 31. 23:05

ResultBuilder

  • Swift 5.4에 도입
  • return 부분의 옵션을 설정하여 return 키워드를 없이 사용할수 있거나, 콤마를 사용하지 않고 배열을 만들 수 있는 등의 기능 사용 가능
  • ex) 여러 표현식을 단일 값으로 결합하여 결과를 빌드하는 경우
resultBuilder를 사용하지 않은 경우 - 콤마 존재 o resultBuilder를 사용한 경우 - 콤마 존재 x
func getPersonMock() -> [Person] {
  [
    Person(name: "jake", age: 20),
    Person(name: "kim", age: 22),
    Person(name: "paul", age: 32)
  ]
}
@PersonBuilder
func getPerson() -> [Person] {
  Person(name: "jake", age: 20)
  Person(name: "kim", age: 22)
  Person(name: "paul", age: 32)
}
  • ex) return 키워드를 사용하지 않아도 동작
   
private func getPerson() -> Person {
  builder { // <- return 키워드 x
    if num % 2 == 0 {
      Person(name: "jake", age: 2) // <- return 키워드 x
    } else {
      Person(name: "jake", age: 1) // <- return 키워드 x
    }
  }
}
private func getPersonNoResultBuilder() -> Person {
  build { // 컴파일 오류 !
    if num % 2 == 0 {
      Person(name: "jake", age: 2) // 경고: 사용되지 않는 변수
    } else {
      Person(name: "jake", age: 1) //  경고: 사용되지 않는 변수
    }
  }
}

ResultBuilder 사용 예시

ex) Person이라는 구조체에서 name값 뒤에 "🍎"을 붙인 name으로 리네임하는 프로그램

struct Person {
  var name: String
  let age: Int
}
  • ResultBuilder를 사용하지 않은 경우
    // 기능
    func build(_ components: Person...) -> [Person] {
      components.map { Person(name: $0.name + "🍎", age: $0.age) }
    }
    
    // 사용
    func getPersonMock() -> [Person] {
      [
        Person(name: "jake", age: 20),
        Person(name: "kim", age: 22),
        Person(name: "paul", age: 32)
      ]
    }
    
    let sample = getPersonMock()
    print(sample)
    
    // [ExDynamicCell.Person(name: "jake", age: 20), ExDynamicCell.Person(name: "kim", age: 22), ExDynamicCell.Person(name: "paul", age: 32)]​


  • ResultBuilder를 사용한 경우 콤마를 사용하지 않아도 가능
    • @resultBuilder 키워드를 이용하여 Builder 컴포넌트 정의
    • 키워드를 이용하면, static func buildBlock(_ components:) 메소드를 강제 재정의가 필요
      @resultBuilder
      struct PersonBuilder {
        static func buildBlock(_ components: Person...) -> [Person] {
          components.map { Person(name: $0.name + "🍎", age: $0.age) }
        }
      }​
    • @resultBuilder키워드가 등록된 PersonBuilder를 사용 -> 여러 표현식을 단일 값으로 결합하여 결과를 빌드
      (콤마를 사용하지 않아도 결합)
      @PersonBuilder
      func getPerson() -> [Person] {
        Person(name: "jake", age: 20)
        Person(name: "kim", age: 22)
        Person(name: "paul", age: 32)
      }​

응용 - builder 만들어서 사용하기

> return 키워드를 사용하지 않아도 builder를 통해 리턴임을 암시하여 컴파일 에러가 뜨지 않으므로 간결한 코드 형성

> if문, else문 안에서도 return 키워드를 사용하지 않아도 리턴임을 암시

private func getPerson() -> Person {
  builder { // <- return 키워드 없어도 가능
    if num % 2 == 0 {
      Person(name: "jake", age: 2) // <- return 키워드 없어도 가능
    } else {
      Person(name: "jake", age: 1) // <- return 키워드 없어도 가능
    }
  }
}
  • builder의 3가지 메소드를 정의
    • buildBlock(_:): result를 컴바인하기 위한 필수로 정의가 필요한 메소드
    • buildEither(first:): if-else와 switch에서 result가 가능하도록하는 메소드 (buildEither(second:)도 하나의 세트처럼 정의)
    • buildEither(second:)
// 출처: https://github.com/SwiftDocOrg/CommonMark/blob/main/Sources/CommonMark/Result%20Builders/ContainerOfBlocksBuilder.swift

#if swift(>=5.4)
@resultBuilder
public struct ContainerOfBlocksBuilder {
    /// Required by every result builder to build combined results from
    /// statement blocks.
    public static func buildBlock(_ components: [Block & Node]...) -> [Block & Node] {
        return components.flatMap { $0 }
    }

    /// If declared, provides contextual type information for statement
    /// expressions to translate them into partial results.
    public static func buildExpression(_ expression: Block & Node) -> [Block & Node] {
        return [expression]
    }

    /// If declared, provides contextual type information for statement
    /// expressions to translate them into partial results.
    public static func buildExpression(_ expression: [Block & Node]) -> [Block & Node] {
        return expression
    }

    /// If declared, provides contextual type information for statement
    /// expressions to translate them into partial results.
    public static func buildExpression(_ expression: Section) -> [Block & Node] {
        return expression.children
    }

    /// If declared, provides contextual type information for statement
    /// expressions to translate them into partial results.
    public static func buildExpression(_ expression: String?) -> [Block & Node] {
        guard let expression = expression, !expression.isEmpty else { return [] }
        
        let document = try? Document(expression)

        // Unlink the children from the document node to prevent dangling pointers to the parent.
        let children = document?.removeChildren() ?? []

        return children
    }

    /// Enables support for `if` statements that do not have an `else`.
    public static func buildOptional(_ component: [Block & Node]?) -> [Block & Node] {
        return component ?? []
    }

    /// With buildEither(second:), enables support for 'if-else' and 'switch'
    /// statements by folding conditional results into a single result.
    public static func buildEither(first component: [Block & Node]) -> [Block & Node] {
        return component
    }

    /// With buildEither(first:), enables support for 'if-else' and 'switch'
    /// statements by folding conditional results into a single result.
    public static func buildEither(second component: [Block & Node]) -> [Block & Node] {
        return component
    }

    /// Enables support for 'for..in' loops by combining the
    /// results of all iterations into a single result.
    public static func buildArray(_ components: [[Block & Node]]) -> [Block & Node] {
        return components.flatMap { $0 }
    }

    /// If declared, this will be called on the partial result of an 'if
    /// #available' block to allow the result builder to erase type
    /// information.
    public static func buildLimitedAvailability(_ component: [Block & Node]) -> [Block & Node] {
        return component
    }
}
#endif
  • resultBuilder를 사용하지 않고 builder 함수를 만들어서 사용하면 compile error 발생
    // resultBuilder를 사용하지 않은 빌더 함수
    
    func build<T>(_ closure: () -> T) -> T { closure() }​

  • resuiltBuilder를 사용
    • @resuiltBuilder로 builder 함수 정의: 제네릭을 이용하여 런타임 시점에, 타입을 선언하고 선언한 타입을 클로저의 리턴값으로 반환
      @resultBuilder
      enum Builder<T> {
        static func buildBlock(_ component: T) -> T { component }
        static func buildEither(first component: T) -> T { component }
        static func buildEither(second component: T) -> T { component }
      }
      
      func builder<T>(@Builder<T> _ closure: () -> T) -> T { closure() }​
    • 사용: return 키워드 없이 builder {  } 블록을 통해 바로 사용 가능
      private func getPerson() -> Person {
        builder {
          if num % 2 == 0 {
            Person(name: "jake", age: 2)
          } else {
            Person(name: "jake", age: 1)
          }
        }
      }​

* 전체 소스 코드

import UIKit

struct Person {
  var name: String
  let age: Int
}

@resultBuilder
enum Builder<T> {
  static func buildBlock(_ component: T) -> T { component }
  static func buildEither(first component: T) -> T { component }
  static func buildEither(second component: T) -> T { component }
}
func builder<T>(@Builder<T> _ closure: () -> T) -> T { closure() }

class ViewController: UIViewController {
  let num = 1
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // 빌더를 사용하지 않은 경우
//    let num = 1
//    let value: Person
//    if num % 2 == 0 {
//      value = Person(name: "jake", age: 1)
//    } else {
//      value = Person(name: "paul", age: 2)
//    }
//    print(value)
    
    // 빌더를 사용한 경우
    let num = 1
    let value: Person = builder {
      if num % 2 == 0 {
        Person(name: "jake", age: 2)
      } else {
        Person(name: "jake", age: 1)
      }
    }
    print(value)
  }
  
  private func getPerson() -> Person {
    builder {
      if num % 2 == 0 {
        Person(name: "jake", age: 2)
      } else {
        Person(name: "jake", age: 1)
      }
    }
  }
  
  // 컴파일 에러!
//  private func getPersonNoResultBuilder() -> Person {
//    build {
//      if num % 2 == 0 {
//        Person(name: "jake", age: 2)
//      } else {
//        Person(name: "jake", age: 1)
//      }
//    }
//  }
}

// result builder를 사용하지 않은 빌더 함수
func build<T>(_ closure: () -> T) -> T { closure() }

* 참고

https://medium.com/bootcampers/swift-result-builder-building-declarative-stackview-320f588fa47b

https://www.raywenderlich.com/books/swift-apprentice/v7.0/chapters/20-result-builders

Comments