SwiftUI 3.0. Третья часть

20 сентября 2021

Продолжаем серию статей, посвящённых новым фичам SwiftUI 3.0. В этой публикации рассмотрим пример создания тулбара для клавиатуры, новый модификатор .task(), который позволяет запускать задачи асинхронно в момент отображения представлений, тип данных AsyncImage для загрузки изображений из сети, а так же рассмотрим набор инструментов для создания эффекта размытия любого фона.

SwiftUI 3.0. AsyncImage: Загрузка изображений из сети

SwiftUI теперь имеет структуру AsyncImage, которая создана специально для загрузки изображений из сети. В простейшей форме её можно использовать просто передав URL-адрес, прямо в инициализатор:

AsyncImage(url: URL(string: "https://developer.apple.com/news/images/og/swiftui-og-twitter.png"))

Обратите внимание, что URL-адрес не является опциональным. Если в инициализатор передать невалидный адрес или же ссылка окажется не действительной, то AsyncImage просто вернет плейсхолдер серого цвета.

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

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

Лучшим решением в этом случае будет использование соответствующих модификаторов для ограничения размера изображения и настройки отображения плейсхолдера:

AsyncImage(url: URL(string: "https://developer.apple.com/news/images/og/swiftui-og-twitter.png")) { image in
    image.resizable()
} placeholder: {
    Color.red
}
.frame(width: 500, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 25))

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

AsyncImage(url: URL(string: "https://developer.apple.com/news/images/og/swiftui-og-twitter.png@2x.png"), scale: 2)

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

SwiftUI 3.0. Использование эффекта размытия

SwiftUI имеет до боли простой эквивалент UIVisualEffectView, который сочетает в себе ZStack, модификатор background() и ряд встроенных материалов.

Рассмотрим пример в котором поверх изображения должен отображаться какой-то текст. При этом к изображению под текстом нужно применить эффект размытия:

ZStack {
    Color.red

    Text("Learn Swift")
        .padding()
        .font(.largeTitle)
        .background(.thinMaterial)
}

Прозрачность размытия зависит от "толщины" материала, которую можно выбрать из заданного набора:

  • .ultraThinMaterial
  • .thinMaterial
  • .regularMaterial
  • .thickMaterial
  • .ultraThickMaterial

Если используется secondary foreground color, то SwiftUI автоматически настроит цвет текста так, чтобы он имел яркий эффект и выделялся на фоне:

ZStack { Color.red Text("Learn Swift") .foregroundColor(.secondary) .padding() .font(.largeTitle) .background(.ultraThinMaterial) }

SwiftUI 3.0. Toolbar для клавиатуры

Наконец-то в SwiftUI появилась возможность настроить тулбар для клавиатуры, на котором можно размещать любые пользовательские действия. Это особенно полезно при работе с цифровой клавиатурой, так как по умолчанию она не содержит кнопки Return. Надо сказать, что модификатор toolbar() в SwiftUI был доступен и ранее, но его не возможно было использовать для работы именно с клавиатурой. Сейчас же достаточно определить значение .keyboard для параметра placement при инициализации ToolbarItemGroup:

struct ContentView: View {
    @State private var name = "Taylor"

    var body: some View {
        TextField("Enter your name", text: $name)
            .textFieldStyle(.roundedBorder)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Button("Click me!") {
                        print("Clicked")
                    }
                }
            }
    }
}

Для того что бы скрыть клавиатуру на тулбаре можно разместить кнопку "Done" и связать её с логическим значением свойства @FocusState:

struct ContentView: View {
    @State private var name = "Taylor Swift"
    @FocusState var isInputActive: Bool

    var body: some View {
        NavigationView {
            TextField("Enter your name", text: $name)
                .textFieldStyle(.roundedBorder)
                .focused($isInputActive)
                .toolbar {
                    ToolbarItemGroup(placement: .keyboard) {
                        Spacer()

                        Button("Done") {
                            isInputActive = false
                        }
                    }
                }
        }
    }
}

@FocusState так же можно использовать для работы с кнопками "Назад" и "Вперед" для перемещения курсора между текстовыми полями.

  • В Xcode beta 4 данный функционал работает только с NavigationView

SwiftUI 3.0. Модификатор .task(). Запуск асинхронной задачи в момент отображения представления

Наравне с модификатором onAppear(), который позволяет запускать код в момент появления представления, появился еще один более продвинутый модификатор task(), позволяющий запускать код асинхронно. Более того, запущенная задача будет автоматически остановлена, как только представление выгрузится из памяти.

Данный модификатор является отличным местом для выполнения сетевых запросов. Давайте рассмотрим пример с получением данных от сервера, которые надо распарсить в массив с моделями данных, с последующим отображением элементов массива в списке:

struct Course: Decodable {
    let name: String
    let imageUrl: URL
}

struct ContentView: View {
    @State private var courses: [Course] = []

    var body: some View {
        NavigationView {
            List(courses, id: \.name) { course in
                HStack {
                    AsyncImage(url: course.imageUrl) { image in
                        image.resizable()
                    } placeholder: {
                        Color.gray
                    }
                    .frame(width: 120, height: 80)
                        
                    Text(course.name)
                }
            }
            .navigationTitle("Courses")
        }
        .task {
            do {
                guard let url = URL(
                    string: "https://swiftbook.ru//wp-content/uploads/api/api_courses"
                ) else { return }
                
                let (data, _) = try await URLSession.shared.data(from: url)
                courses = try JSONDecoder().decode([Course].self, from: data)
            } catch {
                courses = []
            }
        }
    }
}

Модификатор task() можно вызвать из любого представления иерархии в независимости от того, является ли представление текущим или порождает новые. В любом случае задача будет запущена в момент появления вью.

Рассмотрим этот случай на следующем примере, где пользователю нужно просматривать исходный код предоставленных сайтов:

struct ContentView: View {
    private let sites = ["apple.com", "swiftbook.ru", "swift.org"]

    var body: some View {
        NavigationView {
            List(sites, id: \.self) { site in
                NavigationLink(site) {
                    SourceViewer(site: site)
                }
            }
            .navigationTitle("View Source")
        }
    }
}

struct SourceViewer: View {
    @State private var sourceCode = "Loading…"
    
    let site: String

    var body: some View {
        ScrollView {
            Text(sourceCode)
                .font(.system(.body, design: .monospaced))
        }
        .task {
            guard let url = URL(string: "https://\(site)") else { return }

            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                sourceCode = String(decoding: data, as: UTF8.self)
                    .trimmingCharacters(in: .whitespacesAndNewlines)
            } catch {
                sourceCode = "Failed to fetch site."
            }
        }
    }
}

 

Совет: Модификатор task() позволяет запускать, как синхронный, так и асинхронный код, поэтому он подойдет во всех случаях, тогда как onAppear() можно использовать только для синхронного кода.

 

В туториале использованы материалы из:

 

 

 

Содержание