Вот посмотрите на конечный продукт. Многие из вас, кто использовал Instagram, будут знакомы. И что самое приятное, это не обязательно строго относиться к публикации фотографий в социальных сетях. Вы можете использовать этот тип представления, чтобы продемонстрировать что угодно. Это могут быть товары, новостные статьи или другие фотографии. Давайте начнем!

Перед тем, как начать, рассмотрите возможность подписки по этой ссылке, и если вы не читаете ее на TrailingClosure.com, приходите к нам как-нибудь!

Начиная

Если вы работаете в Xcode, вам понадобится несколько фотографий для этого проекта. Я собрал несколько из раздела Nature Unsplash для использования в этом уроке. Вы можете скачать их здесь.

Когда они у вас есть, запустите Xcode, создайте новый проект и добавьте фотографии в папку Assets.xcassets.

Создание LoadingRectangle представления

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

  1. Начните с создания нового представления SwiftUI под названием LoadingRectangle.
  2. Удалите созданный для вас Text и замените его на GeometryReader. Это даст вам ссылку на кадр, который мы будем использовать через секунду.
  3. Затем, чтобы сложить оба прямоугольника, добавьте ZStack в GeometryReader и поместите внутрь двух Rectangle Views. Убедитесь, что вы выровняли ZStack .leading, чтобы наша верхняя Rectangle росла с левой стороны.

Вот что у вас должно получиться на данный момент:

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .leading) {
            Rectangle()
            Rectangle()
        }
    }
}

Затем нам нужно изменить верх Rectangle, чтобы он со временем менял размер. Для этого нужно объявить переменную progress.

var progress: CGFloat

Затем измените второй Rectangle так, чтобы его ширина изменялась в соответствии с переменной progress.

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .leading) {
            Rectangle()
            Rectangle()
                .frame(width: geometry.size.width * self.progress, height: nil, alignment: .leading)
        }
    }
}

Наконец, вы можете стилизовать прямоугольники по своему вкусу. Я пошел дальше и изменил радиус их углов, а также цвет. Окончательный код body ниже.

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .leading) {
            Rectangle()
                .foregroundColor(Color.white.opacity(0.3))
                .cornerRadius(5)

            Rectangle()
                .frame(width: geometry.size.width * self.progress, height: nil, alignment: .leading)
                .foregroundColor(Color.white.opacity(0.9))
                .cornerRadius(5)
        }
    }
}

Объединение изображений и LoadingRectangle

Перейдем к ContentView.swift. Xcode сгенерировал этот файл при создании проекта.

Начните с объявления массива имен изображений вверху. Это должны быть названия фотографий, которые мы добавили ранее, и изображения, которые мы отображаем в «Истории».

var imageNames:[String] = ["image01","image02","image03","image04","image05","image06","image07"]

Подобно тому, что мы сделали в LoadingRectangle, замените Text на ZStack, завернутый в GeometryReader, и поместите внутрь Image и горизонтальный стек LoadingRectangle.

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .top) {
            Image(self.imageNames[0])
                .resizable()
                .edgesIgnoringSafeArea(.all)
                .scaledToFill()
                .frame(width: geometry.size.width, height: nil, alignment: .center)
                .animation(.none)
            HStack(alignment: .center, spacing: 4) {
                ForEach(self.imageNames.indices) { x in
                    LoadingRectangle(progress: 1.0)
                        .frame(width: nil, height: 2, alignment: .leading)
                        .animation(.linear)
                }
            }.padding()
        }
    }
}

Я пошел вперед и добавил несколько вещей ради экономии времени, но не стесняйтесь вернуться и поэкспериментировать с некоторыми настройками, чтобы почувствовать то, что я сделал (в частности, с модификаторами для изображения и рамки для LoadingRectangle)

На данный момент мы добавили в приведенный выше код два заполнителя.

  1. Для текущего отображаемого изображения. Image(self.imageNames[0])
  2. За прогресс на каждом LoadingRectangle. LoadingRectangle(progress: 1.0)

Чтобы двигаться вперед, нам нужно создать StoryTimer, который управляет перемещением от фото к фото, а также передает его прогресс в LoadingRectangle.

Создание StoryTimer

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

class StoryTimer: ObservableObject {
    
    @Published var progress: Double
    private var interval: TimeInterval
    private var max: Int
    private let publisher: Timer.TimerPublisher
    private var cancellable: Cancellable?
    
    
    init(items: Int, interval: TimeInterval) {
        self.max = items
        self.progress = 0
        self.interval = interval
        self.publisher = Timer.publish(every: 0.1, on: .main, in: .default)
    }
    
    func start() {
        self.cancellable = self.publisher.autoconnect().sink(receiveValue: {  _ in
            var newProgress = self.progress + (0.1 / self.interval)
            if Int(newProgress) >= self.max { newProgress = 0 }
            self.progress = newProgress
        })
    }
}

Взгляните на инициализатор для StoryTimer. Требуется количество элементов в нашей «Истории» и TimeInterval, сколько времени мы должны отображать каждый элемент.

Когда появится наш ContentView, мы вызовем функцию start(), чтобы начать получать значения из TimerPublisher. Каждый раз при получении нового значения мы обновляем переменную progress, которая публикуется нашим классом StoryTimer. Это, в свою очередь, заставляет наш ContentView обновлять наш LoadingRectangle с правильным прогрессом и отображает правильное изображение на экране.

Удаление заполнителей в ContentView

Сначала создайте экземпляр StoryTimer в верхней части ContentView.

@ObservedObject var storyTimer: StoryTimer = StoryTimer(items: 7, interval: 3.0)

Затем замените строку, в которой вы объявили Image, на следующую:

Image(self.imageNames[Int(self.storyTimer.progress)])

Что это значит, так это захватить прогресс из StoryTimer и выбрать соответствующее изображение через его индекс.

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

LoadingRectangle(progress: min( max( (CGFloat(self.storyTimer.progress) - CGFloat(x)), 0.0) , 1.0) )

Может показаться, что здесь много чего происходит, но на самом деле это всего лишь простая математика. Диапазон значений self.storyTimer.progress от 0 до N (количество отображаемых фотографий). В то время как Loadingrectangle требуется значение прогресса от 0.0 до 1.0. Мы выполняем преобразование на основе индекса каждого LoadingRectangle (в данном случае x)

Запуск таймера
Наконец, внизу ZStack в ContentView.swift вам нужно запустить StoryTimer, когда появится представление.

var body: some View {
    GeometryReader { geometry in
        ZStack(alignment: .top) {
            /* Image and LoadingRectangles Here */
        }
        .onAppear { self.storyTimer.start() }
        .onDisappear {self.storyTimer.cancel() }

    }
}

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

Дополнительный кредит

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

Добавьте TapGesture в ContentView.swift

Добавьте следующий HStack внизу ZStack внутри тела вашего основного ContentView.

HStack(alignment: .center, spacing: 0) {
    Rectangle()
        .foregroundColor(.clear)
        .contentShape(Rectangle())
        .onTapGesture {
            self.storyTimer.advance(by: -1)
    }
    Rectangle()
        .foregroundColor(.clear)
        .contentShape(Rectangle())
        .onTapGesture {
            self.storyTimer.advance(by: 1)
    }
}

Изменить StoryTimer

Затем нам нужно добавить функцию к классу StoryTimer, которая позволяет нам увеличивать или уменьшать его прогресс. Мы хотим перейти к следующей фотографии, но не забегать слишком далеко вперед и сократить время ее показа. Функция ниже вычисляет текущий индекс элемента, а затем увеличивает прогресс (или вычитает, если вы укажете отрицательное число).

func advance(by number: Int) {
    let newProgress = max((Int(self.progress) + number) % self.max , 0)
    self.progress = Double(newProgress)
}

Все сделано

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

Поддержка будущих сообщений

Если вам понравился этот пост, рассмотрите возможность подписки на мой веб-сайт, используя эту ссылку, и если вы не читаете это на TrailingClosure.com, приходите к нам как-нибудь!