apple/Docs, iOS, Swift

iOS TipKit 사용 예제 정리

lgvv 2024. 8. 18. 16:32

iOS TipKit 사용 예제 정리

  • 목차
    • Overview
    • TipKit 이론
    • Tipkit 예제

Overview

  • TipKit을 사용하면 앱의 새로운 기능에 대해 사용자에게 알리거나 작업을 더 빠르게 수행하는 방법을 보여줄 수 있음.
  • 각 예제는 TipKit 프레임워크의 다양한 기능을 강조.

 

추천하는 경우

 

  • 추천하는 팁 예시
    • actionable > 버튼 통해 추가 정보 혹은 기능 실행
    • instructinal > 기능에 대한 설명
    • easy to remember > 손쉽게 기억 되어야 함.

 

비추천하는 경우

 

  • 비추천하는 팁 예시
    • promotinal > 광고
    • error message > 에러 메시지
    • not actionable > 새로운 기능 알리는 것 외에 할 수 있는게 없음.
    • too complicated > 너무 복잡함.

 

TipKit 코드 사용 기본

  • 기본적으로 Tip과 관련한 이벤트가 앱 시작 혹은 특정 시점에 `configure` 해야함.
    • 아래 코드에서 setupTips() 함수에 주목
// SwiftUI

import SwiftUI
import TipKit

@main
struct TipKitExamples: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

    init() {
        do {
            try setupTips()
        } catch {
            print("Error initializing tips: \(error)")
        }
    }

    // Various way to override tip eligibility for testing.
    // Note: These must be called before `Tips.configure()`.
    private func setupTips() throws {
        // 모든 팁 노출
        // Tips.showAllTipsForTesting()

        // 특정 팁만 테스팅을 위해 노출
        // Tips.showTipsForTesting([tip1, tip2, tip3])

        // 앱에 정의 된 모든 팁을 숨김
        // Tips.hideAllTipsForTesting()

        // 팁과 관련한 모드 데이터를 삭제
        try Tips.resetDatastore()

        // 모든 팁을 로드
        try Tips.configure()
    }
}

 

 

InlineTip

TipView를 View의 원하는 위치에 작성하면 된다.

InlineTip

 

import SwiftUI
import TipKit

struct InlineTip: Tip {
    var title: Text {
        Text("Save as a Favorite")
            .foregroundStyle(.indigo)
    }

    var message: Text? {
        Text("Your favorite backyards always appear at the top of the list.")
    }

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

struct InlineView: View {
    // Create an instance of your tip content.
    let inlineTip = InlineTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("A TipView embeds itself directly in the view. Make this style of tip your first choice as it doesn't obscure or hide any underlying UI elements.")

            // Place your tip near the feature you want to highlight.
            TipView(inlineTip, arrowEdge: .bottom)
            Button {
                // Invalidate the tip when someone uses the feature.
                inlineTip.invalidate(reason: .actionPerformed)
            } label: {
                Label("Favorite", systemImage: "star")
            }

            Text("To dismiss the tip, tap the close button in the upper right-hand corner of the tip or tap the Favorite button to use the feature, which then invalidates the tip programmatically.")
            Spacer()
        }
        .padding()
        .navigationTitle("Inline tip view")
    }
}

#Preview {
    InlineView()
}

 

 

PopoverTip

TipView를 popoverTop modifier에 추가하면 된다.

popoverTip

 

import SwiftUI
import TipKit

struct PopoverTip: Tip {
    var title: Text {
        Text("Add an Effect")
            .foregroundStyle(.indigo)
    }

    var message: Text? {
        Text("Touch and hold \(Image(systemName: "wand.and.stars")) to add an effect to your favorite image.")
    }
}

struct PopoverView: View {
    // Create an instance of your tip content.
    let popoverTip = PopoverTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("Popover views display on top of UI elements. Use this tip view if you don’t want the layout of the screen to change, and are OK with underlying UI elements being obscured or hidden.")
            Text("Lorem ipsum dolor sit amet, consect etur adi piscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
                .foregroundStyle(.tertiary)

            Image(systemName: "wand.and.stars")
                .imageScale(.large)
                // Add the popover to the feature you want to highlight.
                 .popoverTip(popoverTip)
                .onTapGesture {
                    // Invalidate the tip when someone uses the feature.
                    popoverTip.invalidate(reason: .actionPerformed)
                }

            Text("Lorem ipsum dolor sit amet, consect etur adi piscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consect etur adi piscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
                .foregroundStyle(.tertiary)
            Spacer()
        }
        .padding()
        .navigationTitle("Popover tip view")
    }
}

 

ActionTip

Tip에 Action을 설정해 줄수도 있음.

Action은 클로저 및 id 값을 통해 어떤 액션이 클릭 되었는지 알 수 있음.

Alert 사용하는거랑 비슷

ActionView

struct PasswordResetTip: Tip {
    var title: Text {
        Text("Need Help?")
    }

    var message: Text? {
        Text("Do you need help logging in to your account?")
    }

    var image: Image? {
        Image(systemName: "lock.shield")
    }

    var actions: [Action] {
        // Define a reset password button.
        Action(id: "reset-password", title: "Reset Password")
        // Define a FAQ button.
        Action(id: "faq", title: "View our FAQ")
    }
}

struct PasswordResetView: View {
    @Environment(\.openURL)
    var openURL

    // Create an instance of your tip content.
    let passwordResetTip = PasswordResetTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("Use action buttons to link to more options. In this example, two actions buttons are provided. One takes the user to the Reset Password feature. The other sends them to an FAQ page.")
            
            // Place your tip near the feature you want to highlight.
            TipView(passwordResetTip, arrowEdge: .bottom) { action in
                // Define the closure that executes when someone presses the reset button.
                if action.id == "reset-password", let url = URL(string: "https://iforgot.apple.com") {
                    openURL(url) { accepted in
                        print(accepted ? "Success Reset" : "Failure")
                    }
                }

                // Define the closure that executes when someone presses the FAQ button.
                if action.id == "faq", let url = URL(string: "https://appleid.apple.com/faq") {
                    openURL(url) { accepted in
                        print(accepted ? "Success FAQ" : "Failure")
                    }
                }
            }
            Button("Login") {
                // Perform login action.
            }
            Spacer()
        }
        .padding()
        .navigationTitle("Password reset")
    }
}

 

 

ParameterRuleTip - 1

Rule, 즉 특정 규칙에만 팁이 적용되도록 만들수도 있음.

 

@Parameter란 매크로를 붙여주어 특정 조건에서만 사용될 수 있도록

Rule에 파라미터의 조건이 맞는 경우에만 팁이 활성화되게 조절 가능

@Parameter 및 Rule 매크로

 

 

ParameterRules

struct ParameterRuleTip: Tip {
    var title: Text {
        Text("Change Your Photo View")
    }

    var message: Text? {
        Text("Switch between your friend's library and your own.")
    }

    var image: Image? {
        Image(systemName: "photo.on.rectangle")
    }

    var rules: [Rule] {
        // Define a rule based on the app state.
        #Rule(ContentView.$isLoggedIn) {
            // Set the conditions for when the tip displays.
            $0 == true
        }
    }
}

struct ParameterRuleView: View {
    // Create an instance of your tip content.
    let parameterRuleTip = ParameterRuleTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("Use the parameter property wrapper and rules to track app state and control where and when your tip appears.")
            
            // Place your tip near the feature you want to highlight.
            TipView(parameterRuleTip, arrowEdge: .bottom)
            Image(systemName: "photo.on.rectangle")
                .imageScale(.large)

            Button("Tap") {
                // Trigger a change in app state to make the tip appear or disappear.
                ContentView.isLoggedIn.toggle()
            }

            Text("Tap the button to toggle the app state and display the tip accordingly.")
            Spacer()
        }
        .padding()
        .navigationTitle("Parameter rules")
    }
}

struct ContentView: View {
    // Define an app state for showing tips.
    @Parameter
    static var isLoggedIn: Bool = false
    
    var body: some View { 
        Text("A")
    }
}

 

ParameterRuleTip - 2

더 복잡하게도 사용할 수 있음.

 

@Parameter(.transient)에 옵션을 줄수도 있음.

transient 옵션을 줄 경우 처음 참조할 때 기본 값으로 설정함.

즉, 앱을 다시 시작할 때마다 기본값으로 설정 가능.

앱을 재시작 할 때마다 계속 팁이 나타남.

struct AddPlantTip: Tip {
    var title: Text {
        Text("Add a plant to favorites")
    }

    var message: Text? {
        Text("Add plants to your favorites list")
    }

    var image: Image? {
        Image(systemName: "camera.macro")
    }

    var options: [TipOption] {
        MaxDisplayCount(1)
    }
}

struct FavoritePlantTip: Tip {
    // Define a custom value type to store a list of plant names.
    struct FavoritePlants: Codable, Sendable {
        var plants: Set<String> = []

        var arrayValue: [String] {
            Array(plants)
        }

        mutating func setPlants(_ newValue: [String]) {
            plants = Set(newValue)
        }
    }

    // Reset to default value the first time it is referenced.
    @Parameter(.transient)
    static var favoritePlants: FavoritePlants = FavoritePlants(plants: ["Sunflower", "Cactus"])

    var title: Text {
        Text("Explore Favorite Plants")
    }

    var message: Text? {
        Text("Discover your favorite plants and flowers.")
    }

    var image: Image? {
        Image(systemName: "leaf.fill")
    }

    // Tip will only display when there are 3 or more favorite plants and Rose has been favorited.
    var rules: [Rule] {
        // Display if more than two favorite plants are added.
        #Rule(FavoritePlantTip.$favoritePlants) {
            $0.plants.count >= 3
        }

        // Display if "Rose" is added as a favorite.
        #Rule(FavoritePlantTip.$favoritePlants) {
            $0.plants.contains("Rose")
        }
    }
}

struct FavoritePlantsView: View {
    // Collection of all available plants.
    static let allPlants: Set<String> = ["Rose", "Oak", "Maple", "Lily", "Orchid"]

    // Create an instance of your tip content.
    let favoritePlantTip = FavoritePlantTip()
    let addPlantTip = AddPlantTip()

    // Favorited plants.
    @State
    var favorites: [String] = ["Sunflower", "Cactus"]

    var body: some View {
        VStack {
            VStack(alignment: .leading, spacing: 20) {
                Text("Use the parameter property wrapper and rules to track app state and control where and when your tip appears.")

                Text("Tip will only appear when there are at least 3 favorite plants and Rose has been favorited.")
            }
            .padding()
            List {
                ForEach(Array(favorites.enumerated()), id: \.offset) { (idx, plant) in
                    if idx == 0 {
                        Text(plant)
                            .popoverTip(favoritePlantTip)
                    } else {
                        Text(plant)
                    }
                }
            }
            .toolbar {
                ToolbarItem {
                    Button("Add plant") {
                        guard let nextPlant = Self.allPlants.first(where: { !favorites.contains($0) }) else {
                            return
                        }
                        withAnimation {
                            // Trigger a change in app state to make the tip appear or disappear.
                            favorites.append(nextPlant)
                            FavoritePlantTip.favoritePlants.setPlants(favorites)
                        }
                    }
                    .popoverTip(addPlantTip)
                }
            }
            .navigationTitle("Favorite plants")
        }
    }
}

 

EventTip - 1

Event를 사용할 수 있음.

 

앱에서 한 번 이상 발생할 수 있는 동작(예: 사용자의 로그인)을 추적하려는 경우 이벤트를 사용.

동작이 발생할 때 donate()를 사용하여 이벤트에 발생시키고, 이벤트 횟수를 한 번 증가시킬 수 있음.

 

아래 예제에서는 해당 이벤트 아이디를 가진 부분이 dontation 함수 호출에 의해 값이 증가하고 이 Rule을 만족했을 경우 트리거.

< 주의사항 >

- 한번 Rule을 만족하여 트리거 한 후에는 다시 트리거 되지 않음. (무효화되기 때문에)

Event 객체

 

struct EventRuleTip: Tip {
    // Define the user interaction you want to track.
    static let didTriggerControlEvent = Event(id: "didTriggerControlEvent")
    
    var title: Text {
        Text("Control it with a tap.")
    }

    var message: Text? {
        Text("Tap an icon to quickly turn an accessory on or off.")
    }

    var image: Image? {
        Image(systemName: "lock")
    }

    var rules: [Rule] {
        // Define a rule based on the user-interaction state.
        #Rule(Self.didTriggerControlEvent) {
            // Set the conditions for when the tip displays.
            $0.donations.count >= 3
        }
    }
}

struct EventRuleView: View {
    // Create an instance of your tip content.
    let eventRuleTip = EventRuleTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("Use events to track user interactions in your app. Then define rules based on those interactions to control when your tips appear.")
            
            // Place your tip near the feature you want to highlight.
            TipView(eventRuleTip)
            Button(action: {
                // Donate to the event when the user action occurs.
                EventRuleTip.didTriggerControlEvent.sendDonation()
            }, label: {
                Label("Tap three times", systemImage: "lock")
            })
            
            Text("Tap the button above three times to make the tip appear.")
            Spacer()
        }
        .padding()
        .navigationTitle("Events")
    }
}

 

 

OptionTips

옵션을 정해서 팁이 자동으로 무효화되기 전에 몇번까지 나타날 것인지 정해줌

 

OptionTips

 

 

import SwiftUI
import TipKit

struct OptionTip: Tip {
    var title: Text {
        Text("Edit Actions in One Place")
    }

    var message: Text? {
        Text("Find actions such as Copy, Hide, Edit, and Paste under the \(Image(systemName: "ellipsis.circle"))  menu.")
    }

    var image: Image? {
        Image(systemName: "ellipsis.circle")
    }

    var options: [Option] {
        // Show this tip once.
        MaxDisplayCount(10)
    }
}

struct OptionView: View {
    // Create an instance of your tip content.
    let optionTip = OptionTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("Use options to control the frequency your tips appear. For example, this tip is configured to only appear once. If you navigate back and then return, this tip no longer appears until you restart the app.")
            
            // Place your tip near the feature you want to highlight.
            TipView(optionTip, arrowEdge: .bottom)
            Image(systemName: "ellipsis.circle")
                .imageScale(.large)
            
            Text("Tap the button to toggle the app state and display the tip accordingly.")
            Spacer()
        }
        .padding()
        .navigationTitle("Tip options")
    }
}

#Preview {
    OptionView()
}

 

 

 

 

 

(참고)

https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit#Overview

 

Highlighting app features with TipKit | Apple Developer Documentation

Bring attention to new features in your app by using tips.

developer.apple.com