Site icon AppTractor

Добавляем обучающие моменты в приложения с помощью TipKit

Когда TipKit был впервые упомянут во время выступления на WWDC 2023, я предположил, что это будет способ отображения приложений в приложении Tips и, возможно, Spotlight. Однако мы получили встроенный фреймворк для добавления небольших обучающих подсказок в ваше собственное приложение на всех платформах с системой правил для отображения на основе условий и синхронизацией на нескольких устройствах через iCloud! Еще лучше то, что Apple сама использует этот компонент в iOS 17, например, в приложениях Messages и Photos.

Сделав в прошлом несколько систем онбординга, я с нетерпением ждал появления этой функции на WWDC 2023. Я был несколько разочарован, когда версия за версией Xcode не содержал фреймворка TipKit. К счастью, в Xcode 15 beta 5 (выпущенной вчера вечером) появился соответствующий фреймворк и документация, позволяющие мне интегрировать подсказки в собственные приложения.

Прежде чем я покажу, как работает TipKit и как его можно использовать в собственных приложениях, приведу ключевой совет Элли Гаттоцци из доклада «Сделайте фичи обнаруживаемыми с помощью TipKit» на WWDC 2023:

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

Итак, давайте создадим нашу первую подсказку!

Примечание: ниже я привел код для SwiftUI и UIKit, но Apple также предоставила способ отображения подсказок в AppKit. Следует отметить, что версии UIKit недоступны для watchOS и tvOS. Также стоит отметить, что во фреймворке TipKit в бета-версии 5 есть несколько ошибок, в частности, связанных с действиями, которые я описал ниже.

1. Создание подсказки в TipKit

Сначала нам необходимо инициировать систему Tips при запуске нашего приложения с помощью функции Tips.configure():

// SwiftUI
var body: some Scene {
    WindowGroup {
        ContentView()
        .task {
            try? await Tips.configure()
        }
    }
}

// UIKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    Task {
        try? await Tips.configure()
    }
    return true
}

Далее мы создаем структуру, определяющую нашу подсказку:

struct SearchTip: Tip {
    var title: Text {
        Text("Add a new game")
    }
    
    var message: Text? {
        Text("Search for new games to play via IGDB.")
    }
    
    var asset: Image? {
        Image(systemName: "magnifyingglass")
    }
}

Наконец, мы показываем ее:

// SwiftUI
ExampleView()
    .toolbar(content: {
        ToolbarItem(placement: .primaryAction) {
            Button {
                displayingSearch = true
            } label: {
                Image(systemName: "magnifyingglass")
            }
            .popoverTip(SearchTip())
        }
    })


// UIKit
class ExampleViewController: UIViewController {
    var searchButton: UIButton
    var searchTip = SearchTip()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Task { @MainActor in
            for await shouldDisplay in searchTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let controller = TipUIPopoverViewController(searchTip, sourceItem: searchButton)
                    present(controller)
                } else if presentedViewController is TipUIPopoverViewController {
                    dismiss(animated: true)
                }
            }
        }
    }
}

Этот код — все, что требуется для отображения нашей подсказки при первом появлении представления:

Есть два способа отображения подсказок:

Если бы мы хотим отображать подсказку In-line, то наш код должен выглядеть следующим образом:

// SwiftUI
VStack {
    TipView(LongPressTip())
}

// UIKit
class ExampleViewController: UIViewController {
    var longPressGameTip = LongPressGameTip()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Task { @MainActor in
            for await shouldDisplay in longPressGameTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let tipView = TipUIView(longPressGameTip)
                    view.addSubview(tipView)
                } else if let tipView = view.subviews.first(where: { $0 is TipUIView }) {
                    tipView.removeFromSuperview()
                }
            }
        }
    }
}

В UIKit также имеется TipUICollectionViewCell для отображения подсказок в представлении коллекции, который должен использоваться и для табличных интерфейсов. Код SwiftUI, безусловно, менее многословен 🤣.

2. Настраиваем подсказки

Можно изменить цвет текста, шрифты, цвет фона, радиус углов и иконки. Виды подсказок также полностью совместимы с темным режимом.

Шрифты и цвет текста

Эти параметры настраиваются в самих структурах подсказок, так как вы возвращаете экземпляры SwiftUI.Text, даже если в конечном счете отображаете подсказку в UIKit или AppKit.

struct LongPressTip: Tip {
    var title: Text {
        Text("Add to list")
            .foregroundStyle(.white)
            .font(.title)
            .fontDesign(.serif)
            .bold()
    }
    
    var message: Text? {
        Text("Long press on a game to add it to a list.")
            .foregroundStyle(.white)
            .fontDesign(.monospaced)
    }
    
    var asset: Image? {
        Image(systemName: "hand.point.up.left")
    }
}

Поскольку и заголовок, и сообщение используют Text, можно использовать любые модификаторы, возвращающие экземпляр текста, такие как foregroundStyle, font, а также удобные методы типа bold(). Иконка возвращается в виде изображения, поэтому, если мы хотим изменить что-либо, например, цвет иконки, мы должны сделать это в самом представлении Tip.

Цвет иконки, цвет фона и цвет кнопки закрытия

// SwiftUI
TipView(LongPressGameTip())
    .tipBackground(.black)
    .tint(.yellow)
    .foregroundStyle(.white)

// UIKit
let tipView = TipUIView(LongPressGameTip())
tipView.backgroundColor = .black
tipView.tintColor = .yellow

Для изменения цвета фона подсказки предусмотрен метод, но для изменения цвета иконки необходимо использовать глобальный tint, в то время как цвет кнопки закрытия зависит от foregroundStyle. Обратите внимание, что эта кнопка на 50% прозрачна, поэтому если вы используете темный фон, то вряд ли сможете увидеть что-то, кроме белого. Похоже, что в UIKit нет возможности изменить цвет этой кнопки.

Хотя пока не существует Human Interface Guideline для подсказок, просмотр бета-версии iOS 17 и выступления на WWDC 2023 показывает, что Apple использует незаполненные символы SF для всех своих подсказок. По этой причине я бы посоветовал поступить так же!

Радиус скругления

// SwiftUI
TipView(LongPressGameTip())
    .tipCornerRadius(8)

По умолчанию радиус угла для типсов в iOS равен 13. Если вы хотите изменить его, чтобы он соответствовал другим изогнутым элементам в вашем приложении, вы можете сделать это с помощью функции tipCornerRadius() в SwiftUI. В UIKit нет возможности изменить радиус угла для отображения подсказок.

Кастомная подсказка с новыми цветами и шрифтами. Я знаю, что это не очень красиво!

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

3. Свет, камера, действие

Подсказки позволяют добавить несколько кнопок, называемых действиями, которые могут использоваться для перехода к соответствующим настройкам или более подробному руководству. Эта функция недоступна в tvOS.

Чтобы добавить действие, сначала необходимо настроить структуру подсказки, указав некоторые идентифицирующие данные:

// SwiftUI
struct LongPressGameTip: Tip {
    
    // [...] title, message, asset
    
    var actions: [Action] {
        [Action(id: "learn-more", title: "Learn More")]
    }
}

Обратите внимание, что в инициализаторе Action также есть возможность использовать не строку, а блок Text, что позволяет настраивать все цвета и шрифты, о которых говорилось ранее.

Кнопка действия в представлении совета

После этого мы можем изменить вид подсказки таким образом, чтобы она выполняла действие после нажатия кнопки:

// SwiftUI
Button {
    displayingSearch = true
} label: {
    Image(systemName: "magnifyingglass")
}
    .popoverTip(LongPressGameTip()) { action in
        guard action.id == "learn-more" else { return }
        displayingLearnMore = true
    }

// UIKit
let tipView = TipUIView(LongPressGameTip()) { action in
    guard action.id == "learn-more" else { return }
    let controller = TutorialViewController()
    self.present(controller, animated: true)
}

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

var actions: [Action] {
    [Action(id: "learn-more", title: "Learn More", perform: {
        print("'Learn More' pressed")
    })]
}

Важно: несмотря на то, что в Xcode 15 beta 5 можно добавлять действия, обработчики не срабатывают при нажатии на кнопку, независимо от того, используется ли для их подключения метод struct или view.

И последнее, что следует отметить в отношении действий, — их можно отключить, если по каким-то причинам вы хотите их убрать (например, если пользователь не вошел в систему или не подписался на премиум-функции):

var actions: [Action] {
    [Action(id: "pro-feature", title: "Add a new list", disabled: true)]
}

4. Определяем правила TipKit

По умолчанию подсказки появляются сразу после того, как на экране появляется представление, к которому они привязаны. Однако вы можете не показывать подсказку в определенном представлении до тех пор, пока не будет выполнено какое-то условие (например, пока пользователь не вошел в систему), или вы можете захотеть, чтобы пользователь взаимодействовал с функцией определенное количество раз, прежде чем появится подсказка. К счастью, компания Apple подумала об этом и добавила концепцию, известную как «правила», которая позволяет ограничить время появления подсказок.

Существует два типа правил:

Важно: В Xcode 15 beta 5 обнаружена ошибка, из-за которой макрос @Parameter не компилируется в симуляторах или приложениях macOS. Для решения этой проблемы необходимо добавить следующее значение в настройку сборки «Other Swift Flags»:

-external-plugin-path $(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/lib/swift/host/plugins#$(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/bin/swift-plugin-server

Правила, основанные на параметрах

struct LongPressGameTip: Tip {
    
    @Parameter
    static var isLoggedIn: Bool = false
    
    var rules: [Rule] {
        #Rule(Self.$isLoggedIn) { $0 == true }
    }
    
    // [...] title, message, asset, actions, etc.
    
}

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

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

LongPressGameTip.isLoggedIn = true

Правила, основанные на событиях

struct LongPressGameTip: Tip {
    
    static let appOpenedCount = Event(id: "appOpenedCount")
        
    var rules: [Rule] {
        #Rule(Self.appOpenedCount) { $0.donations.count >= 3 }
    }
    
    // [...] title, message, asset, actions, etc.
    
}

Правила, основанные на событиях, несколько отличаются тем, что вместо параметра мы используем объект Event с выбранным нами идентификатором. Правило проверяет свойство donations этого события, чтобы определить, было ли приложение открыто три или более раз. Для того чтобы это правило работало, нам необходимо иметь возможность сделать “donate” при наступлении этого события. Для этого мы используем метод donate в самом событии:

SomeView()
    .onAppear() {
        LongPressTip.appOpenedCount.donate()
    }

Свойство donation в событии содержит свойство date, которое устанавливается в то время, когда событие было “пожертвовано”. Это означает, что можно добавить правила для проверки того, что кто-то открыл приложение сегодня три раза или более:

struct LongPressGameTip: Tip {
    
    static let appOpenedCount: Event = Event(id: "appOpenedCount")
        
    var rules: [Rule] {
        #Rule(Self.appOpenedCount) {
            $0.donations.filter {
                Calendar.current.isDateInToday($0.date)
            }
            .count >= 3
        }
    }
    
    // [...] title, message, asset, actions, etc.
    
}

Важно: Несмотря на то, что этот код должен работать, согласно докладу WWDC 2023, при запуске в Xcode 15 beta 5 он выдает сообщение «функция фильтра не поддерживается в этом правиле».

5. Отображать или не отображать?

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

Частота

По умолчанию подсказки появляются сразу же, как только им разрешено отображение. Это можно изменить, задав параметр DisplayFrequency при инициализации структур Tips при запуске приложения:

try? await Tips.configure(options: {
    DisplayFrequency(.daily)
})

В этом случае каждый день будет отображаться только один совет.

Существует несколько предопределенных значений DisplayFrequency, таких как .daily и .hourly, но вы также можете указать TimeInterval, если вам нужно что-то индивидуальное. В качестве альтернативы можно восстановить поведение по умолчанию, используя значение .immediate.

Если вы установили не мгновенную частоту отображения, но у вас есть совет, который вы хотите отобразить немедленно, вы можете сделать это с помощью опции IgnoresDisplayFrequency() в структуре Tip:

struct LongPressGameTip: Tip {
    
    var options: [TipOption] {
        [Tip.IgnoresDisplayFrequency(true)]
    }
    
    // [...] title, message, asset, actions, etc.
    
}

Счетчик отображения

Если подсказка не закрыта пользователем вручную, то она будет показана вновь при следующем появлении соответствующего представления даже после запуска приложения. Чтобы избежать повторного показа подсказки пользователю, можно задать значение MaxDisplayCount, которое ограничит количество показов до прекращения показа:

struct LongPressGameTip: Tip {
    
    var options: [TipOption] {
        [Tip.MaxDisplayCount(3)]
    }
    
    // [...] title, message, asset, actions, etc.
    
}

 Отмена

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

longPressGameTip.invalidate(reason: .userPerformedAction)

Существует три причины, по которым подсказка может быть признана недействительной:

Первые две причины системные и зависят от количества показов или действий пользователя. Это означает, что при аннулировании подсказок всегда следует использовать .userPerformedAction.

iCloud синхронизация

Во время своего доклада Чарли Паркс упомянул:

TipKit также может синхронизировать статус подсказок через iCloud, чтобы подсказки, отображаемые на одном устройстве, не отображались на другом. Например, если кто-то, использующий приложение, установил его как на iPad, так и на iPhone, и функции идентичны на обоих этих устройствах, вероятно, лучше не информировать их об одной фиче на обоих устройствах.

Эта функция, по-видимому, включена по умолчанию, и нет возможности ее отключить, а это означает, что вам нужно будет предоставить настраиваемые идентификаторы для каждой подсказки на поддерживаемых вами платформах, если вы хотите убедиться, что подсказки по какой-то причине повторно отображаются на каждом устройстве (т.е. если пользовательский интерфейс значительно отличается между устройствами).

6. Отладка

TipKit предоставляет удобные API-интерфейсы для тестирования, позволяя отображать или скрывать подсказки по мере необходимости, проверять все подсказки без соблюдения их правил или очищать всю информацию в хранилище данных TipKit для исходного состояния сборки приложения.

// Show all defined tips in the app
Tips.showAllTips()

// Show the specified tips
Tips.showTips([searchTip, longPressGameTip])

// Hide the specified tips
Tips.hideTips([searchTip, longPressGameTip])

// Hide all tips defined in the app
Tips.hideAllTips()

Если мы хотим очистить все данные, связанные с TipKit, нам необходимо использовать модификатор DatastoreLocation при инициализации фреймворка Tips при запуске приложения:

try? await Tips.configure(options: {
    DatastoreLocation(.applicationDefault, shouldReset: true)
})

Заключение

Подсказка, отображаемая в моем приложении Chaise Longue to 5K для tvOS

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

Источник

Exit mobile version