[SwiftUI] Builder in Swift

정유진·2023년 3월 16일
0

swift

목록 보기
17/24
post-thumbnail

Builder in Swift

struct ContentView: View {

	var body: some View {
    	VStack {
        	Text(text1)
            Text(text2)
        }
    }
}
        
  • some View를 Return 하는 body는 SwiftUI의 View 프로토콜이 필수 구현해야 하는 변수
  • 어떤 함수를 impl하느냐에 따라 Swift가 type을 추론하기에 some으로 type-erased 되어있다. (조만간 Any, some 같은 type-eraser의 장단점을 이야기 해볼 것)
  • var body는 computed property인데 함수를 나열하는 것 만으로 Swift는 하나의 통합된 view를 제공하고 있다.
  • 위의 예시처럼 VStack, HStack, Group은 closure안에 새로운 인스턴스를 생성, 나열하는 것만으로도 그룹핑을 할 수 있다.

1) ViewBuilder

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {

    /// Builds an empty view from a block containing no statements.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view through unmodified.
    ///
    /// An example of a single view written as a child view is
    /// `{ Text("Hello") }`.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
  • 선언부를 살펴보다보면 재미있는 점이 많아서 소개해보려 한다.
  • viewBuilder 안에서 if 분기를 통해 조건에 따라 view를 다르게 그릴 수 있는 이유는 아래와 같은 함수가 있기 때문이다.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    /// Provides support for “if” statements in multi-statement closures,
    /// producing an optional view that is visible only when the condition
    /// evaluates to `true`.
    public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View

    /// Provides support for "if" statements in multi-statement closures,
    /// producing conditional content for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures,
    /// producing conditional content for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}
  • viewBuilder의 parameter의 수는 10개로 제한되어 있다ㅋㅋ 🧐
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}

2) Result Builder

Swift 5.4에서 공식적으로 소개 된 result builder는 5.1에서 소개 된 funciton builder가 더 발전된 형태이다. 우리가 VStack, HStack 클로저 안에서 자식 뷰 인스턴스 선언을 차례로 나열할 때, 그 sequence에 따라 tuple을 생성해 그룹핑하고 하나의 자식 view로 돌려주는 wrapper이다. result builder를 사용하면 나만의 stack을 만드는 것이 가능하다.

@resultBuilder
struct StringStack {
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: "\n")
    }
        
    static func buildEither(first component: String) -> String {
        return component
    }
    
    static func buildEither(second component: String) -> String {
        return component
    }
    
}

사용은 아래와 같이,

@StringStack func fullSentence() -> String {
    "this is my beginning"
    
    if true {
        "truth will be revealed"
    } else {
        "it will be never happened"
    }
    
    "the end"
}

print(fullSentence())

결과는 아래와같다. 단순히 string을 나열한 것에 불과하지만 joined(separator:\n) 처리를 통해 하나의 통합된 String 객체를 반환받았다. 어떻게 써먹을지에 대해서는 좀더 고민을 해봐야겠다. 일단은 SwiftUI에서 제공하는 기본 View들이 이런 식으로 build pattern을 가지고 있다는 점을 짚어보았다.

this is my beginning
truth will be revealed
the end

Builder Design Pattern

자바 생태계에서는 흔하게 접하게 되는 디자인 패턴인 builder pattern는 객체를 생성할 때에 적용할 수있는 디자인 패턴이고 아래와 같이 사용된다.

1) 내가 생성하려는 class

class MyStruct {
	private String requiredMember;
	private int optionalMember1;
	private boolean optionalMember2;

	public String getRequiredMember() {
		return requiredMember1;
	}

	public int getOptionalMember1() {
		return this.optionalMember1;
	}

	public boolean getOptionalMember1() {
		return this.optionalMember2;
	}


	private MyStruct(MyStructBuilder builder) {
		this.requiredMember = builder.getRequiredMember();
		this.optionalMember1 = builder.getOptionalMember1();
		this.optionalMember2 = builder.getOptionalMember2();
	}
}
  • 생성자가 private이므로 builder를 통해서만 객체를 생성할 수 있을 것이다.
  • 멤버 변수의 초기화 또한 빌더를 통해 이루어진다.

2) class builder

public static class MyStructBuilder {

	private String requiredMember;
	private int optionalMember1;
	private boolean optionalMember2;


	public MyStructBuilder(String member) {
		this.requiredMember = member;
	}

	public String getRequiredMember() {
		return this.requiredMember;
	}

	public int getOptionalMember1() {
		return this.optionalMember1;
	}

	public boolean getOptionalMember1() {
		return this.optionalMember2;
	}

	public MyStructBuilder setOptionalMember1(int member) {
		this.optionalMember1 = member;
		return this;
	}

	public MyStructBuilder setOptionalMember2(boolean member) {
		this.optionalMember2 = member;
		return this;
	}

	public MyStruct build() {
		return new MyStruct(this);
	}
}
  • 멤버를 set하는 함수에서 `return this'를 하여 자기 자신을 반환하기 때문에 메서드 체이닝이 가능해진다.
  • 마지막 build 함수를 통해 내가 생성하고자 했던 MyStruct object가 반환된다.

이게 맞나? 왜 쓰죠?

괜히 코드 수만 늘어나는 것처럼 보이는데... 장점은 다음과 같다.

state set이 builder를 통해서만 가능하기 때문에 state가 mutable 해지는 것을 방지하고 예측 가능성을 높인다.

property가 mutable 할 때 생길 수 있는 문제점

class NoteViewController: UIViewController {
    private let note: NSMutableAttributedString // 우리의 state

...
    private func append(_ text: Character) {
        let string = NSAttributedString(
            string: String(text),
            attributes: myTextAttribute
        )

        note.append(string)
    }

    private func save() {
        mydatabase.save(note) {
            ...
        }
    }
}
  • 사용자가 입력하는 문자열에 폰트, 색상 등의 스타일을 입히기 위해 NSAttributedString을 적용하려한다. (Swift에는 아직 대응되는 클래스가 없다)
  • 유저가 문자를 입력할 때마다 AttributedString을 추가하기 위해 note 멤버를 NSMutableAttributedString으로 선언했다. 해당 클래스는 content를 mutate할 수 있는 메서드를 제공하고 있다. (https://developer.apple.com/documentation/foundation/nsmutableattributedstring)
  • 만약, 데이터베이스가 저장하는 중에 note가 mutating되는 경우, 무결한 데이터가 저장된다는 보장을 할 수 없다.
  • 이는 mutable한 property인 note에 동시 접근하여 값을 공유하기 때문에 생길 수 있는 문제점이다.

해결방안

private let noteBuilder = AttributedStringBuilder()

private func appendCharacter(_ text: Character) {
	noteBuilder.append(text, attributes: myTextAttribute)
}

private func save() {
	let finalnote = noteBuilder.build()
    mydatabase.save(finalnote)
  • builder pattern을 사용해 note의 set/get이 동시에 이루어질 가능성을 배제한다.
  • builder를 통해 객체의 state를 set한 후 immutable한 객체를 돌려받는 것이 보장되므로 위의 문제점이 해결된다.
  • 이 외에도 복잡한 객체 set up 과정을 builder 안에 은닉할 수 있다.

참고자료

profile
느려도 한 걸음 씩 끝까지

0개의 댓글