Сделать привязку в представлениях и предварительных просмотрах SwiftUI так же просто, как ABC

Вот несколько распространенных проблем SwiftUI, которые попадают в категорию «должен быть лучший способ».

Дополнительные значения

Начнем с опций. В нашем коде часто используются необязательные значения из-за требований API, но работа с ними в SwiftUI может быть немного болезненной. Учитывать.

class SomeViewModel: ObservableObject {
    @Published var name: String?
}

struct SomeView: View {
    @StateObject var viewModel = SomeViewModel()
    var body: some View{
        TextField("Name", text: $viewModel.name) // does not compile
    }
}

Здесь у нас есть модель представления с необязательной строкой имени, и мы хотим иметь возможность редактировать это значение. К сожалению, нет необязательных инициализаторов для привязки TextField, и компилятор Swift выдаст нам ошибку.«Невозможно преобразовать значение типа 'Binding‹String?›' в ожидаемый тип аргумента 'Binding‹String›'».

Что делать? Я имею в виду, что если бы это было представление Text, мы бы просто использовали оператор объединения nil, чтобы предоставить значение по умолчанию для рассматриваемой строки.

Но как указать значение по умолчанию для привязки?

Привязка расширений

Возможно, вы догадались о части решения, основываясь на названии статьи. Да, мы собираемся создать расширение для Binding.

Вот код.

extension Binding {
    public func defaultValue<T>(_ value: T) -> Binding<T> where Value == Optional<T> {
        Binding<T> {
            wrappedValue ?? value
        } set: {
            wrappedValue = $0
        }
    }
}

И теперь, когда это на месте, мы просто делаем.

struct SomeView: View {
    @StateObject var viewModel = SomeViewModel()
    var body: some View{
        TextField("Name", text: $viewModel.name.defaultValue(""))
    }
}

Все, что нам нужно сделать, это присвоить нашей строке значение по умолчанию, и все готово. Расширение предоставит значение имени, если оно существует, или значение по умолчанию, если оно не существует. В любом случае это выглядит как необязательная привязка к текстовому полю, поэтому наш код теперь работает, как и ожидалось.

А так как наше расширение defaultValue является универсальным, оно работает со строками, целыми числами, логическими значениями… со всем, что нам нужно.

Пустые строки

Если мы работаем с большим количеством строк и если мы всегда устанавливаем их по умолчанию пустыми, мы могли бы позаимствовать концепцию, которую использовал RxSwift, когда ему нужно было сделать необязательные привязки текста.

Давайте добавим еще одно расширение в наш репертуар.

extension Binding where Value == Optional<String> {
    public var orEmpty: Binding<String> {
        Binding<String> {
            wrappedValue ?? ""
        } set: {
            wrappedValue = $0
        }
    }
}

Который позволяет…

struct SomeView: View {
    @StateObject var viewModel = SomeViewModel()
    var body: some View{
        TextField("Name", text: $viewModel.name.orEmpty)
    }
}

То же, что и defaultValue(""), только немного меньше кода.

Привязка в превью SwiftUI

Давайте продолжим и посмотрим на следующее удивительно сложное представление SwiftUI.

struct AmazinglyComplexView: View {

    @Binding var value: Bool

    var body: some View {
        Toggle(isOn: $value) {
            Text("Toggle Me")
        }
        .padding()
    }
}

Здесь у нас есть представление с Toggle, которое имеет привязку к некоторому значению. Теперь мы хотим просмотреть наше представление. Легко, верно?

struct AmazinglyComplexView_Previews: PreviewProvider {
    static var previews: some View {
        AmazinglyComplexView(value: .constant(true))
    }
}

Поскольку у нас есть привязка, мы следуем соглашениям SwiftUI и передаем .constant(true) в параметр привязки нашего представления. И как только мы это сделаем, мы увидим предварительный просмотр нашего представления.

Однако есть только одна проблема. В Xcode 14 предварительные просмотры теперь активны… но мы не можем переключать наше представление и видеть изменение нашего переключателя, так как мы передали постоянное значение для нашей привязки. Нажатие переключателя ничего не дает.

Что делать?

Посмотреть обертки

Что ж, если вы посмотрите на StackOverflow, вы увидите, что общий консенсус заключается в том, чтобы обернуть наше представление другим представлением, в котором определена переменная @State, а затем передать привязку к этому значению. на наш взгляд.

Вы знаете, представление, которое мы хотели протестировать в первую очередь. Похоже на это.

struct AmazinglyComplexView_Previews: PreviewProvider {
    struct Wrapper: View {
        @State var value: Bool
        var body: some View {
            AmazinglyComplexView(value: $value)
        }
    }
    static var previews: some View {
        Wrapper(value: true)
    }
}

На самом деле, это подход, рекомендованный Apple в Видео для разработчиков WWDC 2020.

В любом случае результат один и тот же, и наш предварительный просмотр работает.

Но действительно? Должны ли мы каждый раз проделывать подобную канитель, когда хотим предварительно просмотреть представление, содержащее привязку?

Дженерики спешат на помощь!

Я полагаю, мы могли бы похвастаться своим SwiftUI-забавой и написать какое-то общее представление. Что-то, что оборачивает наше представление предварительного просмотра в ViewBuilder и создает объект состояния соответствующего типа. Возможно, что-то вроде…

struct StateWrapper<Value, Content:View>: View {
    @State var value: Value
    let content: (_ value: Binding<Value>) -> Content
    init(value: Value, @ViewBuilder content: @escaping (_ value: Binding<Value>) -> Content) {
        _value = .init(initialValue: value)
        self.content = content
    }
    var body: some View {
        content($value)
    }
}

struct AmazinglyComplexView_Previews: PreviewProvider {
    static var previews: some View {
        StateWrapper(value: true) {
            AmazinglyComplexView(value: $0)
        }
    }
}

И бинго! У нас есть другое решение. Своего рода. Я имею в виду, по крайней мере, теперь нам не нужно создавать оболочку каждый раз, когда мы хотим протестировать какое-то представление с одной привязкой…

Ой. Черт.

Что, если нам нужно протестировать представление с двумя привязками? Или три? Начнем ли мы писать StateWrappers с привязками один, два, три и четыре?

Должен быть более простой метод.

Переменная

SwiftUI был очень, очень близок к тому, чтобы предоставить то, что нам было нужно с .constant.

Но, как вы, наверное, знаете, закрыть счет можно только в подковы, ручные гранаты и когда ваш дезодорант перестает действовать. Имея это в виду, позвольте мне представить .variable.

Добавьте следующее расширение в свой код.

extension Binding {
    public static func variable(_ value: Value) -> Binding<Value> {
        var state = value
        return Binding<Value> {
            state
        } set: {
            state = $0
        }
    }
}

Здесь мы создаем функцию расширения, которая присваивает наше начальное значение внутренней переменной состояния. Замыкания get и set привязки захватывают эту переменную состояния и изменяют ее по мере необходимости.

И вуаля! Теперь, когда вы хотите проверить свое представление в предварительном просмотре, просто сделайте это.

struct AmazinglyComplexView_Previews: PreviewProvider {
    static var previews: some View {
        AmazinglyComplexView(value: .variable(true))
    }
}

И теперь мы можем переключать Toggle нашего представления на свое усмотрение.

Более того, с .variable мы можем использовать их столько и так часто, как это необходимо.

Обратите внимание, что этот метод не идеален. Самый большой недостаток заключается в том, что это не приведет к полному обновлению представления, поэтому любое другое представление, зависящее от значения нашего переключателя, не будет обновляться.

Я точно знаю? В любом случае, переменная по-прежнему полезна при создании прототипа, и вы всегда можете вернуться к использованию StateWrapper, если возникнет такая необходимость.

Больше ручных гранат

Учитывая указанные выше недостатки .variable в представлениях рендеринга, я включаю несколько дополнительных универсальных оболочек привязки для представлений, требующих до четырех разных привязок.

struct Bindings<Value, Content:View>: View {
    @State var value: Value
    let content: (_ value: Binding<Value>) -> Content
    init(value: Value, @ViewBuilder content: @escaping (_ value: Binding<Value>) -> Content) {
        _value = .init(initialValue: value)
        self.content = content
    }
    var body: some View {
        content($value)
    }
}

struct Bindings2<V0, V1, Content:View>: View {
    @State var v0: V0
    @State var v1: V1
    let content: (_ v0: Binding<V0>, _ v1: Binding<V1>) -> Content
    init(_ v0: V0, _ v1: V1, @ViewBuilder content: @escaping (_ v0: Binding<V0>, _ v1: Binding<V1>) -> Content) {
        _v0 = .init(initialValue: v0)
        _v1 = .init(initialValue: v1)
        self.content = content
    }
    var body: some View {
        content($v0, $v1)
    }
}

struct Bindings3<V0, V1, V2, Content: View>: View {
    @State var v0: V0
    @State var v1: V1
    @State var v2: V2
    let content: (_ v0: Binding<V0>, _ v1: Binding<V1>, _ v2: Binding<V2>) -> Content
    init(_ v0: V0, _ v1: V1, _ v2: V2,
         @ViewBuilder content: @escaping (_ v0: Binding<V0>, _ v1: Binding<V1>, _ v2: Binding<V2>) -> Content) {
        _v0 = .init(initialValue: v0)
        _v1 = .init(initialValue: v1)
        _v2 = .init(initialValue: v2)
        self.content = content
    }
    var body: some View {
        content($v0, $v1, $v2)
    }
}

struct Bindings4<V0, V1, V2, V3, Content: View>: View {
    @State var v0: V0
    @State var v1: V1
    @State var v2: V2
    @State var v3: V3
    let content: (_ v0: Binding<V0>, _ v1: Binding<V1>, _ v2: Binding<V2>, _ v3: Binding<V3>) -> Content
    init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3,
         @ViewBuilder content: @escaping (_ v0: Binding<V0>, _ v1: Binding<V1>, _ v2: Binding<V2>, _ v3: Binding<V3>) -> Content) {
        _v0 = .init(initialValue: v0)
        _v1 = .init(initialValue: v1)
        _v2 = .init(initialValue: v2)
        _v3 = .init(initialValue: v3)
        self.content = content
    }
    var body: some View {
        content($v0, $v1, $v2, $v3)
    }
}

Просто выберите тот, который вам нужен.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Bindings2(true, "Mike") {
            AnotherComplexView(value: $0, name: $1)
        }
    }
}

И если вы передаете более четырех разных привязок… вы можете подумать о рефакторинге некоторого кода.

Просто говорю.

Блок завершения

Итак, у вас есть это. Ничего революционного сегодня, просто пара общих проблем с привязкой SwiftUI, с которыми мы часто сталкиваемся в нашем коде, и некоторые из моих любимых расширений привязки и оберток представлений, которые позволяют нам сократить их до размера.

Есть свои любимые? Дайте мне знать о них в комментариях ниже.

До скорого.

Эта статья является частью Серии SwiftUI.