Как стать автором
Обновить

Пишем интерактивный виджет

Уровень сложностиСредний
Время на прочтение13 мин
Количество просмотров4.5K
Всего голосов 20: ↑20 и ↓0+20
Комментарии6

Комментарии 6

для меня как чайника в iOS было интересно, поместил в закладки.

Добавил ли эпл для нас возможность как-то отлавливать нажатия кнопки/свича, чтобы мы понимали, когда произошло нажатие для обновления ui?
То есть на примере вашего виджета: пользователь нажимает на звездочку -> мы это отлавливаем, перекрашиваем в серый -> слово успешно добавляется в избранное -> перекрашиваем звездочку в золотой, а затем через секунду обратно в начальный цвет, при неуспешной записи в красный.
Да, у вас работа завязана на UserDefaults, и в принципе по смыслу не требуется, но просто интересно знать возможно ли вообще реализовать такое поведение в новых виджетах? Тк думаю, что в задачах, где нужно обработать результат запроса в сеть, такое будет полезно

Добавил ли эпл для нас возможность как-то отлавливать нажатия кнопки/свича, чтобы мы понимали, когда произошло нажатие для обновления ui?

— Отлавливать нажатия в виджете во View в привычном смысле нельзя. Потому что каждое нажатие на Button/Toggle триггерит перерисовку всей вьюхи: в терминах виджета, один снепшот с данными перерисовывается на другой. Поэтому завязаться на булеву переменную и при нажатии на Toggle менять состояние не получится

Но Apple добавила другой способ, через который можно обработать этот случай, через задание стилей для Button/Toggle: ButtonStyle и ToggleStyle аналогично. Необходимо дописать свой стиль

На примере Toggle:

// Описываем кастомный стиль для Toggle
struct CheckToggleStyle: ToggleStyle {
    
    var onToggleImage: (Bool) -> Image
    var imageColor: Color
    
    init(imageColor: Color,
         _ onToggleImage: @escaping (Bool) -> Image) {
        self.onToggleImage = onToggleImage
        self.imageColor = imageColor
    }
    
    func makeBody(configuration: Configuration) -> some View {
        Button {
            configuration.isOn.toggle()
        } label: {
            Label {
                self.onToggleImage(configuration.isOn)
                    .resizable()
                    .renderingMode(.template)
                    .foregroundColor(self.imageColor)
            } icon: { }
        }
        .buttonStyle(.plain)
    }
}

Создаём Toggle во вью. Из-за особенностей работы с виджетом, делаем с захардкоженными данными. Это позволительно, так как после нажатия на Toggle, его состояние будет всё равно меняться.

Toggle("",
       isOn: false,
       intent: SwipeAppIntent())
  .toggleStyle(CheckToggleStyle(imageColor: .softYellow) { isOn in
      Image(uiImage: (isOn ? .starFilledIcon : .starUnfilledIcon) ?? UIImage())
    })
  .frame(width: 30.0, height: 30.0)

И для красоты, устанавливаем задержку в 0.3 секунды в AppIntent

struct SwipeAppIntent: AppIntent {
    
    static var title: LocalizedStringResource = "Swipe card"
    static var isDiscoverable: Bool = false
    
    func perform() async throws -> some IntentResult {
        // Спим 0.3 секунды
        try await Task.sleep(nanoseconds: 300_000_000)

        // Продолжаем работу
        let key = "group.Revolvetra.Inc.Ficha"
        let storage: WordsStorageProtocol = WordsStorage(key: key)
        storage.swipeCard()
        return .result()
    }
}

Toggle кнопки избранное
Toggle кнопки избранное

Перекрашиваем звездочку в золотой, а затем через секунду обратно в начальный цвет, при неуспешной записи в красный

— Такой случай, в предыдущем решении вряд ли можно обработать. Есть единственный вариант, в случае ошибки внутри метода perform в AppIntent выкинуть ошибку (throw Error), тк сигнатура метода позволяет это сделать func perform() async throws -> some IntentResult
В таком случае, виджет перезагрузится, но так как данные не поменялись, то вью моргнёт

Спасибо за статью и подробный ответ)
Будет интересно почитать от вас про динамичный конфиг)

НЛО прилетело и опубликовало эту надпись здесь

Можно сделать, как property wrapper. Кому как нравится)
Мне показался странным синтаксис инициализации переменной с нижним подчёркиванием, обёрнутой property wrapper, поэтому сделал через обычный generic

class SomeClass {
    
    @UserDefaultCodable var someVar: Int
    
    init(key: String) {
        self._someVar = .init(key: key, defaultValue: 0)
    }
}

Зарегистрируйтесь на Хабре, чтобы оставить комментарий