Option Set 및 Sorcery를 사용하여 손쉬운 SwiftUI 보기 바인딩



토글이 많은 SwiftUI 보기를 본 적이 있습니까?

struct LotsOfStateView: View {
    //1
    @State var showBackground = false
    //2
    @State var showBorder = false
    //3
    @State var reduceMotionEnabled = false
    //4
    @State var largeFont = false
    //5
    @State var receiveEmailNotifications = false

    var body: some View {
        VStack {
            Toggle(isOn: $showBackground) {
                Text(LocalizedStringKey("Show Background"))
            }
            Toggle(isOn: $showBorder) {
                Text(LocalizedStringKey("Show Border"))
            }
            Toggle(isOn: $reduceMotionEnabled) {
                Text(LocalizedStringKey("Reduce Motion Enabled"))
            }
            Toggle(isOn: $largeFont) {
                Text(LocalizedStringKey("Large Font"))
            }
            Toggle(isOn: $receiveEmailNotifications) {
                Text(LocalizedStringKey("Receive Notifications"))
            }
        }
        .padding()
        .font(.system(size: 22, weight: .light, design: .rounded))
    }
}


이러한 토글을 처리하는 것은 꽤 간단한 작업이지만 더 많이 추가할수록 번거로울 수 있습니다.

Bool 값의 양을 줄이는 기술은 하나의 저장 단위로 압축하는 것입니다. 본질적으로 그것들을 일련의 0과 1로 변환할 수 있습니다. 익숙한 것 같나요?

There are only 10 types of people in the world – those who understand binary, and those who don’t.



따라서 위의 @State를 보다 관리하기 쉬운 것으로 변환하려면 Swift의 OptionSet를 사용할 수 있습니다. 그것들은 enums 와 매우 유사합니다.

이전에 미리 알림을 설정하셨을 것입니다. 예를 들어 알림 빈도(월요일, 수요일 및 토요일)를 설정하는 경우. 기본적으로 3개의 값 집합을 생성합니다.

원하는 값의 조합을 생성할 수 있다는 이 기본 아이디어는 우리가 보기에 있는 상용구 상태의 양을 제거하는 데 완벽합니다.

위의 @State를 OptionSet으로 변환하려면 다음과 같이 할 수 있습니다.

struct Options: OptionSet {
    var rawValue: UInt
    static let none = Self([])
    static let showBackground = Self(rawValue: 1 << 0)
    static let showBorder = Self(rawValue: 1 << 1)
    static let reduceMotionEnabled = Self(rawValue: 1 << 2)
    static let largeFont = Self(rawValue: 1 << 3)
    static let receiveEmailNotifications = Self(rawValue: 1 << 4)
}


그런 다음 우리의 관점에서 하나를 위해 모든 @State를 제거할 수 있습니다.

@State var viewOptions = Options.none


OptionSet의 rawValue를 보면 UInt입니다. 본질적으로 값의 합집합을 하나의 단일 저장 값으로 저장하고 있습니다. (101001 계열, 온/오프)

이것이 귀하의 보기와 같습니다.

struct LessStateVariablesView: View {
    @State var viewOptions = Options.none

    var body: some View {
        ZStack {
            if $viewOptions.bindValue(.showBackground) {
                Color.red.edgesIgnoringSafeArea(.all)
            }
            VStack {
                Toggle(isOn: $viewOptions.bind(.showBackground)) {
                    Text(LocalizedStringKey("Show Background"))
                }
                Toggle(isOn: $viewOptions.bind(.showBorder)) {
                    Text(LocalizedStringKey("Show Border"))
                }
                Toggle(isOn: $viewOptions.bind(.reduceMotionEnabled)) {
                    Text(LocalizedStringKey("Reduce Motion Enabled"))
                }
                Toggle(isOn: $viewOptions.bind(.largeFont)) {
                    Text(LocalizedStringKey("Large Font"))
                }
                Toggle(isOn: $viewOptions.bind(.receiveEmailNotifications)) {
                    Text(LocalizedStringKey("Receive Notifications"))
                }
            }
            .padding()
            .font(.system(size: 22, weight: .light, design: .rounded))
        }
    }
}


보시다시피 이전 @State를 모두 이동하고 OptionSet으로 캡슐화했습니다. 동작은 정확히 동일하지만 5개의 ​​bool을 갖는 대신 1을 가지며 모든 true 및 false는 1개의 단일 UInt로 저장됩니다.

OptionSet을 바인딩으로 변환



이제 옵션이 있으므로 SwiftUI 보기에서 옵션과 상호 작용하는 방법이 필요합니다. 예를 들어 토글을 사용하려면 Binding 이 필요합니다.

A property wrapper type that can read and write a value owned by a source of truth.



바인딩은 일반 값Value이 있는 구조체이므로 새 OptionSet과 상호 작용하기 위해 확장을 작성할 수 있습니다.

extension Binding where Value: OptionSet, Value == Value.Element {
    func bindedValue(_ options: Value) -> Bool {
        return wrappedValue.contains(options)
    }

    func bind(
        _ options: Value,
        animate: Bool = false
    ) -> Binding<Bool> {
        return .init { () -> Bool in
            self.wrappedValue.contains(options)
        } set: { newValue in
            let body = {
                if newValue {
                    self.wrappedValue.insert(options)
                } else {
                    self.wrappedValue.remove(options)
                }
            }
            guard animate else {
                body()
                return
            }
            withAnimation {
                body()
            }
        }
    }
}


이렇게 함으로써 이제 SwiftUI 보기에 연결할 수 있습니다.

이제 bindedValue에 액세스하여 Bool 표현을 꺼내고 Toggle에 전달하여 값을 업데이트할 수 있는 bind도 있습니다. 변경 사항을 애니메이션화하기 위한 편리한 옵션animate도 있습니다.

보일러 플레이트 제거(구조용 소스)



OptionSet을 작성하는 것은 매우 사소하지만 각 항목에 필요한 구문과 올바른 항목rawValue을 잊어버리는 경우가 많습니다.

OptionSet을 매우 간단하고 쉽게 만들기 위해 Sourcery와 함께 사용할 수 있는 템플릿 파일Stencil을 만들었습니다.

Sourcery is a code generator for Swift language, built on top of Apple's own SwiftSyntax. It extends the language abstractions to allow you to generate boilerplate code automatically.
Sourcery



지금 우리가 할 수 있는 일은 귀하의 사례로 열거형을 만들고 귀하가 OptionsBinding를 준수하는지 확인하는 것입니다.

protocol OptionsBinding {}

enum MyEnum: OptionsBinding {
    case showBackground
    case showBorder
    case reduceMotionEnabled
    case largeFont
    case receiveEmailNotifications
}


그런 다음 Sourcery는 해당 열거형을 확장하고 이에 상응하는 OptionSet을 추가합니다.

extension MyEnum {
    struct Options: OptionSet {
        var rawValue: UInt
        static let none = Self([])
        static let showBackground = Self(rawValue: 1 << 0)
        static let showBorder = Self(rawValue: 1 << 1)
        static let reduceMotionEnabled = Self(rawValue: 1 << 2)
        static let largeFont = Self(rawValue: 1 << 3)
        static let receiveEmailNotifications = Self(rawValue: 1 << 4)
    }
}


이제 필요할 때마다 MyEnum.Options를 자유롭게 사용할 수 있습니다.

이것이 일부 사람들이 뷰에서 Bool 상용구를 줄이는 데 도움이 되고 좋은 열거형을 사용하여 옵션을 쉽게 추론할 수 있기를 바랍니다.

스텐실 템플릿




{% for type in types.enums where type.cases.count > 0 and type.based.OptionsBinding or type|annotated:"OptionsBinding" %}
extension {{ type.name }} {
    struct Options: OptionSet {
        var rawValue: UInt
        static let none = Self([])
        {% for i in 0...type.cases.count %}
            {% if not forloop.last %}
        static let {{type.cases[i].name}} = Self(rawValue: 1 << {{i}})
      {% endif %}
    {% endfor %}
    }
}
{% endfor %}

extension Binding where Value: OptionSet, Value == Value.Element {
    func bindValue(_ options: Value) -> Bool {
        return wrappedValue.contains(options)
    }

    func bind(
        _ options: Value,
        animate: Bool = false
    ) -> Binding<Bool> {
        return .init { () -> Bool in
            self.wrappedValue.contains(options)
        } set: { newValue in
            let body = {
                if newValue {
                    self.wrappedValue.insert(options)
                } else {
                    self.wrappedValue.remove(options)
                }
            }
            guard animate else {
                body()
                return
            }
            withAnimation {
                body()
            }
        }
    }
}


참조 / 유용한 링크



Source Code
Custom OptionSet
OptionSet
Sourcery

좋은 웹페이지 즐겨찾기