Помните DVD-заставку из начала 2000-х? Она не только была практичным инструментом для предотвращения выгорания экрана на ЭЛТ-дисплеях, но и стала культурной иконой.
Как и многие из вас, я помню, как видел эту заставку в детстве, но совершенно забыл о ней, пока недавно не пересматривал эту сцену из «Офиса»:
Я подумал, что было бы забавно воссоздать эту заставку с помощью SwiftUI. Признаться, для этого нет никакой реальной причины, кроме того, что мне предстоит долгий перелет и нужно убить немного времени.
Давайте приступим!
Работа с Canvas API
Прежде всего, давайте настроим все элементы пользовательского интерфейса. Анимацией мы займемся позже.
struct ContentView: View { @State private var position: CGPoint = .zero private let canvasSize: CGSize = UIScreen.main.bounds.size private let imageSize: CGSize = CGSize(width: 128, height: 76) private let image = Image("dvd_logo") var body: some View { Canvas { [position] context, size in // Set the background color to .black context.fill(Path(CGRect(origin: .zero, size: size)), with: .color(.black)) // Draw image at current position var image = context.resolve(image) image.shading = .color(.red) context.draw( image, in: CGRect(x: position.x, y: position.y, width: imageSize.width, height: imageSize.height) ) } .onAppear { // Set initial position to the center of the canvas after the view appears position = CGPoint(x: (canvasSize.width - imageSize.width) / 2, y: (canvasSize.height - imageSize.height) / 2) } .ignoresSafeArea() } }
Мы можем использовать SwiftUI Canvas
, чтобы задать цвет фона для экрана и нарисовать изображение с заданным цветом фона. Затем, в onAppear
, мы вычисляем центральную точку экрана и используем ее в качестве начального местоположения изображения.
Необходимость группы захвата
На этом этапе я столкнулся с интересным поведением API Canvas.
Если я явно не объявил position
в группе захвата Canvas
, изображение всегда отрисовывается в нулевом положении .zero
, даже если position
обновляется в onAppear
.
Такое поведение становится еще более интересным, если сравнить его с обычным представлением SwiftUI с аналогичной настройкой, где все ведет себя так, как и ожидалось.
В следующем представлении SwiftUI элемент Text
точно отражает центр представления, а представление перерисовывается при изменении значения position
в onAppear
.
struct ContentView: View { @State private var position: CGPoint = .zero private let canvasSize: CGSize = UIScreen.main.bounds.size var body: some View { VStack { Text("\(position.x), \(position.y)") } .onAppear { // Set initial position to the center of the view after it appears position = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2) } } }
Давайте спишем это на разницу в том, как Canvas управляет своими зависимостями, и продолжим нашу реализацию. Если вы знаете, что здесь происходит, я буду рад услышать об этом.
Создание цикла рисования
На данный момент мы успешно воспроизвели внешний вид заставки DVD.
Далее нам нужно будет создать механизм, запускающий перерисовку View
через регулярные промежутки времени (например, 30 кадров в секунду). Кроме того, нам понадобится способ обновлять положение изображения в каждом из этих интервалов.
Чтобы перерисовывать View с частотой 30 кадров в секунду, мы можем просто использовать комбинацию таймера и onReceive
:
private let framesPerSecond: Double = 1 / 30 Canvas { ... } .onReceive(Timer.publish(every: framesPerSecond, on: .main, in: .common).autoconnect()) { _ in // TODO: Update image position here }
Функция autoconnect()
в SwiftUI автоматически подключает издателя (например, Timer
) к представлению, позволяя представлению реагировать на значения, выдаваемые издателем, без ручной настройки. Это упрощает процесс создания реактивных пользовательских интерфейсов за счет автоматической обработки логики подписки и обновления.
Теперь мы знаем, что хотим, чтобы изображение двигалось каждый раз, когда вид перерисовывается, но как быстро оно должно двигаться?
Давайте добавим вектор скорости, чтобы управлять скоростью перемещения логотипа:
@State private var velocity: CGVector = CGVector(dx: 1, dy: 1)
В блоке onReceive
мы обновим position.x
и position.y
, добавив соответствующие значения из velocity
. Эта настройка позволяет нам управлять скоростью движения по осям X и Y независимо друг от друга.
.onReceive(Timer.publish(every: framesPerSecond, on: .main, in: .common).autoconnect()) { _ in // Update position based on velocity self.position.x += self.velocity.dx self.position.y += self.velocity.dy }
Если бы вы запустили код сейчас, то заметили бы, что изображение постепенно уходит за пределы экрана:
Далее мы добавим несколько проверок границ, чтобы наше изображение оставалось в пределах видимой области экрана.
Добавление проверок границ
Как мы должны реагировать, когда наше изображение достигает края?
Если изображение приближается к правому краю экрана, то все, что нам нужно сделать, это изменить направление его горизонтального движения, верно? Аналогично, если изображение приближается к верхней части экрана, нам нужно только перевернуть его вертикальное движение, сохранив горизонтальное движение прежним.
С этим предположением проверка границ становится очень простой:
.onReceive(Timer.publish(every: framesPerSecond, on: .main, in: .common).autoconnect()) { _ in // Update position based on velocity self.position.x += self.velocity.dx self.position.y += self.velocity.dy // Check if image hits a horizontal edge if self.position.x + self.imageSize.width >= canvasSize.width || self.position.x <= 0 { // Flip horizontal direction self.velocity.dx *= -1 } // Check if image hits a vertical edge if self.position.y + self.imageSize.height >= canvasSize.height || self.position.y <= 0 { // Flip vertical direction self.velocity.dy *= -1 } }
Мы уже близки к финалу! Теперь нам нужно менять цвет изображения каждый раз, когда оно сталкивается с краем.
@State private var imageColor: Color = .green // Canvas var image = context.resolve(image) image.shading = .color(imageColor) // .onReceive // Check if image hits a horizontal edge if self.position.x + self.imageSize.width >= canvasSize.width || self.position.x <= 0 { // Flip horizontal direction self.velocity.dx *= -1 self.imageColor = Color.random() } // Check if image hits a vertical edge if self.position.y + self.imageSize.height >= canvasSize.height || self.position.y <= 0 { // Flip vertical direction self.velocity.dy *= -1 self.imageColor = Color.random() } extension Color { static func random() -> Color { let red = Double.random(in: 0...1) let green = Double.random(in: 0...1) let blue = Double.random(in: 0...1) return Color(red: red, green: green, blue: blue) } }
Весь код вы можете найти здесь: https://github.com/aryamansharda/DVDScreensaver
Надеюсь, вам понравилась эта статья! Если понравилось, пожалуйста, поделитесь ею в социальных сетях.