Category: TipKit

  • SwiftUIでTipKitを使用してユーザーにヒントを表示(iOS 17、WWDC 2023)

    この記事では、iOS 17の新しいTipKitフレームワークを使用して、アプリ内のさまざまな機能をユーザーに発見させる方法について説明します。

    tipkit-inline-2.jpg

    本記事の内容は以下の通りです:

    • インラインヒントの表示
    • フローティングヒントの表示
    • 条件に基づくヒントの表示
    • カウンターに基づくヒントの表示
    • ヒントのアクションボタンのカスタマイズ

    この記事は、主にSwiftUIを対象としています。

    この記事は公開されているリソースをもとに書かれており、公開されているAppleのドキュメンテーションからのスクリーンショットや例示コードが含まれる場合があります。
    動作の仕方を確認したい場合は、Xcode 15 beta 8で自身で試してください。

    Tipオブジェクトの定義

    Tipオブジェクトには、画像、タイトル、説明、およびヒントのアクションが含まれています。以下はシンプルなヒントの例です:

    var asset: Image? には、ヒントの左側に表示するオプションの画像を提供できます。
    var title: Text には、このヒントのタイトルとして表示したいSwiftUIのテキストを提供します。
    var message: Text? には、タイトルの下部に表示されるオプションの説明文字列を提供できます。

    import TipKit
    
    struct HashTagPostButtonTip: Tip {
        var image: Image? {
            Image(systemName: "star.bubble")
        }
        var title: Text {
            Text("Send a Quick Response")
        }
        var message: Text? {
            Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
        }
    }
    
    tipkit-floating.jpg

    また、var rules: [Rule] および var actions: [Action] パラメータもありますが、これについては記事の次のセクションで説明します。

    ヒントの表示

    ヒントを表示する方法は2つあります。

    apple-tip-view-ways.jpeg

    (from WWDC video)

    インラインヒント

    ヒントをビュー内に表示することができます。ヒントビューは、あなたの機能(例:ボタン)を指す矢印とともに表示されます。これは、TipViewをビューコードに直接含めることで実現できます:

    tipkit-inline-2.jpg
    import SwiftUI
    import TipKit
    
    struct TipWithArrow: View {
        var body: some View {
            VStack {
                
                HStack {
                    TipView(HashTagPostButtonTip(), arrowEdge: .trailing)
                    
                    Image(systemName: "number")
                        .font(.system(size: 30))
                        .foregroundStyle(.white)
                        .padding()
                        .background { Circle().foregroundStyle(.blue) }
                }
                
            }
            .padding()
            .task {
                try? Tips.configure([
                    .displayFrequency(.immediate),
                    .datastoreLocation(.applicationDefault)
                ])
            }
        }
    }
    
    #Preview {
        TipWithArrow()
    }
    

    arrowEdge パラメータを使用して、矢印の指す方向を決定することができます。
    .trailing に設定すると、ヒントは右側を指す矢印を表示します(つまり、機能ボタンが右側にあります)。
    また、機能ボタンが左側にあり(表示されているヒントがボタンの右側にある場合)、.leadingに設定します。

    フローティング(ポップオーバー)ヒント

    ボタンにフローティングヒントを添付するためのビューモディファイアも使用できます。これは、例えばナビゲーションボタンに対するヒントを表示したい場合に便利です:

    floating-tip.jpg
    struct PopOverTip: View {
        
        var hashtagButtonTip = HashTagPostButtonTip()
        
        var body: some View {
            VStack {
                
                Text("Hello world!")
                
            }
            .padding()
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Image(systemName: "number")
                        .onTapGesture {
                            hashtagButtonTip.invalidate(reason: .actionPerformed)
                        }
                        .popoverTip(hashtagButtonTip)
                }
            }
            .task {
                try? Tips.configure([
                    .displayFrequency(.immediate),
                    .datastoreLocation(.applicationDefault)
                ])
            }
        }
        
    }
    
    #Preview {
        PopOverTip()
    }
    
    

    If you want your app to be compatible also with previous iOS versions, you can use a similar technique as shown below to create a conditional view modifier:

    extension View {
    	@ViewBuilder
    	func tipIfAvailable(with tip: Tip) -> some View {
    		if #available(iOS 17, *) {
    			self
    				.popoverTip(tip)
    		}
    	}
    }
    

    ヒントの頻度

    デフォルトでは、各ヒントは一度だけ表示されます。
    .displayFrequency(.immediate) に設定すると、ユーザーが以前にヒントを見ていない場合、ヒントはすぐに表示されます。
    他の表示頻度も設定することができます。例えば、.hourly に設定すると、システムは1時間に1回以上ヒントが表示されないようにします。
    ヒントデータの保存場所(ヒントが表示されたかどうか)も .datastoreLocation(.applicationDefault) 関数を使用してカスタマイズできます。

    特定の条件が満たされたときにヒントを表示

    特定の条件が満たされた場合にのみヒントを表示することができます。例えば、プレミアム機能のヒントは、ユーザーがプレミアム機能を購入した場合にのみ表示されるべきです。
    Tipオブジェクトの静的変数を定義することができます。その後、その静的変数に値を割り当てることで、ヒントが表示されるかどうかを制御することができます。

    import TipKit
    
    struct PremiumUserOnlyTip: Tip {
        
        @Parameter
        static var isPremiumUser: Bool = false
        
        var image: Image? {
            Image(systemName: "wand.and.rays")
        }
        var title: Text {
            Text("Add an Effect")
        }
        var message: Text? {
            Text("Choose a fun Live Photo effect like Loop or Bounce.")
        }
        
        var rules: [Rule] {
            #Rule(Self.$isPremiumUser) {
                $0 == true
            }
        }
        
    }
    
    

    アプリの起動時に、静的変数 PremiumUserOnlyTip.isPremiumUser を設定することで、ヒントはプレミアムユーザーのみに表示されます。

    ユーザーのインタラクションを記録し、それに応じてヒントを表示

    ユーザーのインタラクションをカウンターとして記録(アプリを使用した回数など)し、ユーザーが要件を満たしたときのみヒントを表示することができます。

    import TipKit
    
    struct UsageFrequencyBasedTip: Tip {
        
        static let numerOfTimesOpened: Event = Event(id: "com.example.TipKit.numberOfTimesOpened")
        
        var image: Image? {
            Image(systemName: "star.fill")
        }
        var title: Text {
            Text("Tap to download HD picture")
        }
        var message: Text? {
            Text("Only for premium users")
        }
        
        var rules: [Rule] {
            #Rule(Self.numerOfTimesOpened) {
                $0.donations.count >= 3
            }
        }
        
    }
    

    上記のコードでは、Eventオブジェクトを使用してアプリを開いた回数を記録します。rulesパラメータでは、Swiftのマクロを使用して、ユーザーがアプリを開いた回数が3回以上の場合にのみヒントが表示されることを示しています。
    これで、アプリ内で以下のコードを使用してカウンターをインクリメントできます:

    Button("tap this button 3 times") {
        Task {
            await UsageFrequencyBasedTip.numerOfTimesOpened.donate()
        }
    }
    

    UsageFrequencyBasedTip.numerOfTimesOpened.donate() 関数を3回呼び出すと、ヒントが表示されます

    カスタムアクション付きヒント

    ヒントのアクションボタンを提供することもできます:

    tip-custom-actions.jpg
    import TipKit
    
    struct TipWithOptions: Tip {
        
        var image: Image? {
            Image(systemName: "star.bubble")
        }
        
        var title: Text {
            Text("Send a Quick Response")
        }
        
        var message: Text? {
            Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
        }
        
        var actions: [Action] {
            return [
                .init(id: "learn-more", title: "Learn more", perform: {
                    print("Learn more tapped")
                }),
                .init(id: "enable-feature", title: "Enable magic feature", perform: {
                    print("Enable feature tapped")
                })
            ]
        }
        
    }

    ヒントのデバッグ

    以下の関数を使用して、ヒントをデバッグすることができます:
    ヒントが表示されたかどうかのストレージデータベースをリセット:

    try? Tips.resetDatastore()

    上記のコードは、Tips/configure(options:)を呼び出す前に呼び出す必要があります。
    ヒントが以前に表示されたかどうかに関係なく、すべてのヒントを表示することもできます:

    Tips.showAllTipsForTesting()

    ヒントが以前に表示されたかどうかに関係なく、特定のヒントを表示する:

    Tips.showTipsForTesting([ExampleTip.self])

    パラメータで、ヒントのタイプの配列を提供します。
    すべてのヒントを隠す、または特定のタイプのヒントのみを隠すこともできます:

    Tips.hideAllTipsForTesting()
    Tips.hideTipsForTesting(_ tips: [Tip.Type])

    サンプルコード

    iOS 17が公開された後、サンプルのXcodeプロジェクトをアップロードします。

    Good & NG

    tipkit-good.jpeg
    tipkit-ng.jpg

    プロモーションのヒントを表示すべきではありません。TipKitを使用してエラーメッセージを表示しないでください。実行可能なアイテム(例:クリック可能な機能ボタン)に関連したヒントのみを表示し、ヒント内に詳細な手順を表示しないでください(NGの例で示されているように。その場合、ユーザーにオプションを提供するためにカスタムアクションボタンを使用するのが最善)。

    (source: public Apple session videos)


    お読みいただき、ありがとうございました。

    ニュースレター: https://blog.mszpro.com

    Mastodon/MissKey: @[email protected] https://sns.mszpro.com

    :relaxed: Twitter @MszPro

    :relaxed: 個人ウェブサイト https://MszPro.com


    上記内容の一部は、Apple社のサンプルコードから引用しています。ライセンスは下記に添付しています:

    Copyright © 2023 Apple Inc.
    
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    

  • Using TipKit to display a tip to the user in SwiftUI for onboarding

    This article is about using the new TipKit framework for iOS 17 to help the user discover various features within your app.

    This article will cover:

    • displaying an inline tip
    • displaying floating tip
    • display tip based on conditions
    • display tip based on a counter

    This article is primarily for SwiftUI.

    This article is written with public resources and may contain screenshots from public Apple documentations and example codes. To see how it works, please try it yourself with the Xcode 15

    Define a Tip object

    Tip object contains the image, title, description, and the actions for a tip. Here is a simple tip:

    import TipKit

    struct HashTagPostButtonTip: Tip {
    var image: Image? {
    Image(systemName: "star.bubble")
    }
    var title: Text {
    Text("Send a Quick Response")
    }
    var message: Text? {
    Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
    }
    }

    In the var asset: Image?, you can provide an optional image to be displayed on the left side of the tip.

    In the var title: Text, you will provide a SwiftUI Text for the text you want to show as the title of this tip.

    In the var message: Text?, you can provide an optional description string that will be displayed at the bottom of the title.

    There is also a var rules: [Rule] and the var actions: [Action]parameter, which we will talk about in the next section of this article.

    Displaying a tip

    There are 2 ways to display a tip.

    image from public documentation on developer.apple.com

    inline tips

    You can display the tip within your view. The tip view will come with an arrow that points to your feature (for example, a button). You can do that by directly including TipView in your view code:

    import SwiftUI
    import TipKit

    struct TipWithArrow: View {
    var body: some View {
    VStack {

    HStack {
    TipView(HashTagPostButtonTip(), arrowEdge: .trailing)

    Image(systemName: "number")
    .font(.system(size: 30))
    .foregroundStyle(.white)
    .padding()
    .background { Circle().foregroundStyle(.blue) }
    }

    }
    .padding()
    .task {
    try? Tips.configure([
    .displayFrequency(.immediate),
    .datastoreLocation(.applicationDefault)
    ])
    }
    }
    }

    #Preview {
    TipWithArrow()
    }

    You can use the arrowEdge parameter to decide which direction the arrow points to. Set to .trailing so that the tip will display an arrow point to the right side (meaning the feature button is on the right); or set it to .leading when the feature button is on the left (and the shown tip is on the right side of the button).

    Floating (popover) tips

    You can also use a view modifier to attach a floating tip to a button. This is useful for example when you want to show a tip for a navigation button:

    struct PopOverTip: View {

    var hashtagButtonTip = HashTagPostButtonTip()

    var body: some View {
    VStack {

    Text("Hello world!")

    }
    .padding()
    .toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
    Image(systemName: "number")
    .onTapGesture {
    hashtagButtonTip.invalidate(reason: .actionPerformed)
    }
    .popoverTip(hashtagButtonTip)
    }
    }
    .task {
    try? Tips.configure([
    .displayFrequency(.immediate),
    .datastoreLocation(.applicationDefault)
    ])
    }
    }

    }

    #Preview {
    PopOverTip()
    }

    Tip frequency

    By default, each tip only shows once.

    When you set .displayFrequency(.immediate), the tip will be immediately shown if the user has not seen the tip before.

    You can also set other display frequencies. For example, if you set it to .hourly, the system makes sure that no more than 1 tip is shown every hour.

    You can also customize where the tip data (whether the tips have been shown or not) by using the .datastoreLocation(.applicationDefault) function.

    Show tip when certain condition is met

    You can only show a tip when certain condition is met. For example, a tip for a premium feature should only be shown if the user has purchased the premium feature.

    You can define a static variable for the Tip object. Then, you can assign values to that static variable to control whether the tip is shown or not.

    import TipKit

    struct PremiumUserOnlyTip: Tip {

    @Parameter
    static var isPremiumUser: Bool = false

    var image: Image? {
    Image(systemName: "wand.and.rays")
    }
    var title: Text {
    Text("Add an Effect")
    }
    var message: Text? {
    Text("Choose a fun Live Photo effect like Loop or Bounce.")
    }

    var rules: [Rule] {
    #Rule(Self.$isPremiumUser) {
    $0 == true
    }
    }

    }

    On app launch, we can set the static variable isPremiumUser so the tip will only be shown for users who is premium.

    PremiumUserOnlyTip.isPremiumUser = isPremiumUser

    Record user interactions and show tips accordingly

    You can record user interactions as a counter (like the number of times that the user used your app) and only show tips when the user has met a requirement.

    import TipKit

    struct UsageFrequencyBasedTip: Tip {

    static let numerOfTimesOpened: Event = Event(id: "com.example.TipKit.numberOfTimesOpened")

    var image: Image? {
    Image(systemName: "star.fill")
    }
    var title: Text {
    Text("Tap to download HD picture")
    }
    var message: Text? {
    Text("Only for premium users")
    }

    var rules: [Rule] {
    #Rule(Self.numerOfTimesOpened) {
    $0.donations.count >= 3
    }
    }

    }

    In the above code, we record the number of times user opened the app using an Event object. In the rules parameter, we use a Swift Macro and indicate that the tip will only be shown if the times user opened is larger than or equal to 3.

    Now, we can increment the counter by using the following code in our app:

    Button("tap this button 3 times") {
    Task {
    await UsageFrequencyBasedTip.numerOfTimesOpened.donate()
    }
    }

    When you call donate() function for 3 times, the tip will be shown.

    Tip with custom actions

    You can also provide your action buttons for a tip:

    import TipKit

    struct TipWithOptions: Tip {

    var image: Image? {
    Image(systemName: "star.bubble")
    }

    var title: Text {
    Text("Send a Quick Response")
    }

    var message: Text? {
    Text("Double-tap a message, then choose a Tapback, like a ♥︎.")
    }

    var actions: [Action] {
    return [
    .init(id: "learn-more", title: "Learn more", perform: {
    print("Learn more tapped")
    }),
    .init(id: "enable-feature", title: "Enable magic feature", perform: {
    print("Enable feature tapped")
    })
    ]
    }

    }

    Debugging tips

    You can debug your tips by using the following function:

    Reset the storage database for whether tips are shown or not:

    try? Tips.resetDatastore()

    The above code must be called before calling Tips/configure(options:)

    You can also show all tips regardless of whether a tip has been shown before:

    Tips.showAllTipsForTesting()

    You can show a specific tip regardless of whether it has been shown before:

    Tips.showTipsForTesting([ExampleTip.self])

    in the parameter, provide an array of the type of the tips.

    You can also hide all tips, or just hide a specific type of tip:

    Tips.hideAllTipsForTesting()
    Tips.hideTipsForTesting(_ tips: [Tip.Type])

    Good vs Not Good

    Good Examples. image from public documentation on developer.apple.com
    NG (Not Good). image from public documentation on developer.apple.com

    vYou should not show promotion tips, do not use TipKit to show error messages, only show tip that is associated with an actionable item (for example, a clickable feature button), and do not show detailed steps within the tip (like shown in the example of the NG)