
| Color App Tip | parSta! Tip |
|---|---|
![]() | ![]() |
디테일업 및 추가기능 구현 중
| Color App Tip View | parSta! Tip View |
|---|---|
![]() | ![]() |
| TipKit을 연습 | 프로젝트에 적용하기 |
오늘은
TipKit을 사용하여 사용자에게 앱 사용법을 알려주는 것을 구현하려고 한다.
이를 위해 컬러 블록을 생성할 수 있고, 사용자가 원하는 컬러블록을 북마크 할 수 있도록contextMenu를 사용할 것이다.
<구현할 Tip>
- 컬러블록을 추가하는 방법 Tip
- 컬러블록을 북마크 하는 방법 Tip
- 앱을 사용하는 방법 Tip핵심코드
import Foundation import TipKit struct AddColorTip: Tip { var title: Text { Text("Add New Color") .foregroundStyle(Color.blue) } var message: Text? { Text("Tap here to add a new color to the list") } var image: Image? { Image(systemName: "paintpalette") } }
이번에는 앱에서 기본적으로 컬러블록을 몇가지 제공하고, 트리거를 통해 사용자가 랜덤한 컬러의 컬러블록을 생성할 수 있도록 한다.
이를 위해 먼저 랜덤한 Color타입의 값을 가질 수 있도록 Color타입에 메서드를 추가한다.
extension Color {
static var random: Color {
Color(
// RGB 컬러의 값을 랜덤으로 가져와 랜덤한 색상이 되도록 설정
red: Double.random(in: 0...1),
green: Double.random(in: 0...1),
blue: Double.random(in: 0...1)
)
}
}
// 사용 - Color.random
다음으로 랜덤한 컬러를 가지는 배열을 만들어준다.
이번에는 배열을 구조체로 먼저 선언하고 static을 통해 인스턴스를 생성하지 않고도 프로퍼티 값을 사용할 수 있도록 하였다.
struct MocData {
static let colors = [Color.random,
Color.random,
Color.random,
Color.random]
}
struct ContentView: View {
@State private var colors = MocData.colors
}
이제 뷰에 배열이 가진 멤버의 수만큼 랜덤한 색의 컬러블록이 생성되도록 ForEach를 사용하여 화면을 구성한다.
ScrollView {
ForEach(colors, id: \.self) {
RoundedRectangle(cornerRadius: 10)
// RoundedRectangle색을 그라데이션으로 설정
.fill($0.gradient)
.frame(height: 100)
}
}
이 때, 생성된 블록을 사용자가 길게 누르면 북마크할 수 있도록 contextMenu를 사용한다.
contextMenu는 SwiftUI에서 제공하는 뷰 수정자(modifier)로, 길게 눌렀을 때 또는 우클릭을 통해 추가 옵션을 제공하는 컨텍스트 메뉴를 표시하는 기능이다.
ScrollView {
ForEach(colors, id: \.self) {
RoundedRectangle(cornerRadius: 10)
// RoundedRectangle색을 그라데이션으로 설정
.fill($0.gradient)
.frame(height: 100)
.contextMenu {
// 컬러블록을 길게 누르면 Favorite 버튼이 표시됨
Button("Favorite", systemImage: "star") {
// code to set as favorte
}
}
}
}
마지막으로 컬러블록을 추가할 트리거를 만든다.
이번에는 '+'버튼을 누르면 랜덤한 색의 컬러블록이 생성되도록 한다.
Button {
// 컬러배열인 colors에 랜덤한 컬러를 추가하여 ForEach를 통해 새 블록 생성
withAnimation {
colors.insert(.random, at: 0)
}
} label: {
Image(systemName: "plus")
}

이렇게 앱의 기본구성이 완료되었기 때문에 이제 Tip에 대한 정의와 선언을 해준다.
Tip을 사용하기 위해서는 TipKit 프레임워크를 선언하여 기능을 사용할 수 있도록 해야한다.
그리고 Tip프로토콜을 준수하는 구조체를 생성하여 사용하는데, 프로토콜을 준수하기 위해 var title:Text를 반드시 선언해야 한다.
import Foundation
import TipKit
struct AddColorTip: Tip {
// 필수 구현
var title: Text {
Text("Add New Color")
.foregroundStyle(Color.blue)
}
var message: Text? {
Text("Tap here to add a new color to the list")
}
var image: Image? {
Image(systemName: "paintpalette")
}
}
팁에 대한 정의가 끝났으면 다시 메인 뷰로 돌아와 팁을 보여주기 위한 작업을 진행한다.
이 때, 메인 뷰도 TipKit 프레임워크를 선언해주어야 한다.
팁을 선언하는 방법은 두 가지가 있다.
| InlineTip | PopoverTip |
|---|---|
![]() | ![]() |
이번에는 PopoverTip으로 팁을 표시한다.
이를 위해 위에서 정의한 Tip 프로토콜을 준수하는 구조체, AddColorTip 타입을 가지는 인스턴스를 선언해준다.
let addColorTip: AddColorTip = AddColorTip()
이번 팁은 컬러블록을 추가하는 방법을 나타내기 때문에 위에서 만든 컬러블록을 추가하는 트리거에 PopoverTip뷰를 지정해준다.
Button {
withAnimation {
colors.insert(.random, at: 0)
}
addColorTip.invalidate(reason: .actionPerformed)
} label: {
Image(systemName: "plus")
}
// 기본 형태 - .popoverTip(tip: (any Tip)?)
.popoverTip(addColorTip)
InlineTip을 구현하는 방법도 어렵지 않다.
우선 PopoverTip을 구현할 때와 마찬가지로 Tip 프로토콜을 준수하는 구조체, AddColorTip 타입을 가지는 인스턴스를 선언해준다.
그리고 원하는 위치에 TipView를 선언해준다.
// 사용자가 컬러블록을 북마크 하는 팁
let setFavoriteTip: SetFavoriteTip = SetFavoriteTip()
// 중략
ScrollView {
// 기본 형태 - TipView(tip: Tip)
TipView(setFavoriteTip)
// 팁 대화상자의 색을 변경
.tipBackground(.teal.opacity(0.2))
ForEach(colors, id: \.self) { ... }
}
팁을 사용하면서 테스트를 하거나 기본 설정을 주기 위해서 아래의 코드를 활용할 수 있다.
// 뷰가 생성되면 자동으로 실행
init() {
do {
try setupTips()
} catch {
print("Error initializing tips: \(error)")
}
}
// Tip을 표시할 때 기본 설정
private func setupTips() throws {
// 모든 팁 노출
// Tips.showAllTipsForTesting()
// 특정 팁만 테스팅일 위해 노출
// Tips.showTipsForTesting([tip1, tip2, tip3])
// 앱에 정의된 모든 팁을 숨김
// Tips.hideAllTipsForTesting()
// 팁과 관련한 모든 데이터를 삭제
// try Tips.resetDatastore()
// 모든 팁을 로드
// try Tips.configure()
}
팁은 사용자에게 앱을 사용하는데 도움을 주지만, 항상 표시가 된다면 오히려 불편을 줄 수 있다.
때문에 필요할 때만 표시되거나 특정 트리거에 의할 때만 팁이 표시되어야 한다.
SwiftUI에서는 Tip 프로토콜의 Rule을 통해 팁이 생성되는 이벤트를 설정할 수 있도록 도와준다.
이번에는 Help 버튼을 만들어 버튼을 누르면 앱 사용 팁이 나오도록 한다.
Rule을 사용하는 방법은 2가지가 있다.
@Parameter 사용하기변수를 선언할 때@Parameter 매크로를 붙여주고, Rule에서 파라미터의 조건이 맞는 경우에만 팁이 활성화 되도록 조절할 수 있다.
먼저 @Parameter를 가지는 변수를 선언해준다.
@Parameter public static var helpCall: Bool = false
다음으로 Tip 프로토콜을 준수하는 구조체에 Rule을 추가한다.
struct HelpTip: Tip {
// 생략...
var rules: [Rule] {
// 컨텐츠뷰의 helpCall이라는 값이 true일 경우에만 작동
#Rule(ContentView.$helpCall) {
$0 == true
}
}
}
이 때, Action 프로퍼티를 통해 팁에서 액션을 줄 수도 있다.
Action은 클로저 및 id 값을 통해 어떤 액션이 클릭 되었는지 알 수 있다.(Alert와 비슷)
var actions: [Action] {
// Define a FAQ button
Action(id: "faq", title: "View our FAQ")
// Define a Help button
Action(id: "start", title: "How to use the app")
}
이제 메인 뷰로 돌아가 팁을 보여줄 트리거로 사용할 버튼을 만들어준다.
Button(action: {
// 버튼이 눌렸는지 확인
self.buttonClicked.toggle()
// 버튼을 누르면 helpCall 값이 변하며 팁 생성
ContentView.helpCall.toggle()
}) {
Text("Help")
.font(.headline)
.fontWeight(.medium)
}
.buttonStyle(.borderedProminent)
마지막으로 InlineTip 방식으로 팁을 뷰에 추가해준다.
표시 조건을 파라메터로 해두었기 때문에 평소에는 보이지 않고 트리거를 통해서만 확인할 수 있다.
// arrowEdgh - 팁에 꼬리를 생성하고 위치를 지정
TipView(helpTip, arrowEdge: .bottom)
그리고 위에서 Action을 선언했기 때문에 이것을 활용하여 사용자가 어떤 액션을 선택했는지에 따라 다른 결과를 줄 수 있다.
// @Environment(\.openURL) var openURL
TipView(helpTip, arrowEdge: .bottom) { action in
// id값이 faq인 액션을 선택했을 경우
// URL을 연결하여 인터넷에 이동되도록 한다.
if action.id == "faq", let url = URL(string: "https://www.google.com") {
openURL(url) { accepted in
print(accepted ? "Success FAQ" : "Failure FAQ")
}
}
// id값이 start인 액션을 선택할 경우
// Alert를 활성화 시킴
if action.id == "start" {
self.isShowing = true
}
}
// 앱의 사용법이 적힌 간단한 알림창 생성
.alert(isPresented: $isShowing) {
Alert(title: Text("Start!!"),
message: Text("Press the '+' button to add color and save your favorite color!"), dismissButton: .cancel())
}

여기서 혹시나 사용자가 help 버튼을 누르지 않아도 시간이 흐르면 자동으로 팁을 추가할 수 있을까? 라고 생각했다.
사용자가 버튼을 누르지 않았음을 확인하고, 그 상태로 5초가 지나면 자동으로 팁을 표시하는 기능을 수행해보려고 한다.
먼저 사용자가 버튼을 눌렀는지 확인하기 위해 변수를 하나 선언한다.
@State private var buttonClicked: Bool = false
그리고 onAppear와 DispatchQueue를 활용하여 뷰가 생성되고 일정 시간이 경과했을 때 실행할 액션을 설정해준다.
.onAppear() {
// 뷰가 생성되고 5초가 지났을 때
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// 사용자가 버튼을 클릭했는가?
guard self.buttonClicked == false else {
return
}
// 클릭하지 않았다면 팁을 표시
self.buttonClicked.toggle()
ContentView.helpCall.toggle()
}
}

앱을 사용하면서 한 번 이상 발생할 수 있는 이벤트(ex: 로그인)를 추적하고 싶을 경우에 Event를 사용한다.
특정 이벤트가 발생할 때 donate를 사용하여 팁의 이벤트 횟수를 증가시킬 수 있다.
이를 활용하여 이번에는 사용자가 컬러블록을 2회 북마크 해야만 팁이 표시되는 것을 구현해볼 예정이다.
먼저 Tip에 룰을 추가한다.
struct SetFavoriteTip: Tip {
// 이벤트 정의
static let setFavoriteEvent = Event(id: "setFavorite")
// 중략...
var rules: [Rule] {
// 이벤트가 발생한 횟수가 1보다 큰 경우 팁 표시
#Rule(Self.setFavoriteEvent) { event in
event.donations.count > 1
}
}
}
이제 이벤트를 발생시키는 조건을 설정한다.
위에서 contextMenu를 통해 구현한 북마크 버튼의 액션에 이벤트 발생 트리거를 선언한다.
이 때, 이벤트는 비동기 작업이기 때문에 Task내부에서 진행해 주어야 한다.
RoundedRectangle(cornerRadius: 10)
.fill($0.gradient)
.frame(height: 100)
.contextMenu {
Button("Favorite", systemImage: "star") {
// code to set as favorte
// 버튼을 누르면 이벤트가 발생
Task {
await SetFavoriteTip.setFavoriteEvent.donate()
}
}
}


오늘 학습한
TipKit을 활용하여 우리 프로젝트인 parSta! 에도 팁을 추가해 보기로 하였다. 조건은 앱에 첫 접속을 했을 때만 메인 뷰의 기능을 설명하는 팁을 만드는 것이다.핵심코드
.onAppear() { Task { await SwiftDataTips.swiftDataTip.donate() await DailyQuizTips.dailyQuizTip.donate() } if SwiftDataTips.swiftDataTip.donations.count > 1 || DailyQuizTips.dailyQuizTip.donations.count > 1 { MainTitleView.isFirstLaunch = false UserDefaults.standard.set(MainTitleView.isFirstLaunch, forKey: "userFirstLaunch") } else { MainTitleView.isFirstLaunch = true UserDefaults.standard.set(MainTitleView.isFirstLaunch, forKey: "userFirstLaunch") } MainTitleView.isFirstLaunch = UserDefaults.standard.bool(forKey: "userFirstLaunch") }
우선 사용할 팁을 정의한다. 이번에는 SwiftData와 DailyQuiz의 사용법을 알려주는 팁만 추가하려고 한다.
import Foundation
import TipKit
struct SwiftDataTips: Tip {
static let swiftDataTip = Event(id: "swiftData")
var title: Text {
Text("Swift Dictionary")
}
var message: Text? {
Text("You can gain basic knowledge of swift")
}
// 파라미터가 트루일 때가 팁 표시
// 사용자가 처음으로 앱을 사용하는가?
var rules: [Rule] {
#Rule(MainTitleView.$isFirstLaunch) {
$0 == true
}
}
}
struct DailyQuizTips: Tip {
static let dailyQuizTip = Event(id: "dailyQuiz")
var title: Text {
Text("Daily Quiz")
}
var message: Text? {
Text("You can test your knowledge. Try to match the problem and build up your experience and rank up!")
}
// 파라미터가 트루일 때가 팁 표시
// 사용자가 처음으로 앱을 사용하는가?
var rules: [Rule] {
#Rule(MainTitleView.$isFirstLaunch) {
$0 == true
}
}
}
메인 뷰로 돌아와 TipView를 선언해주고, Rule로 지정해둔 @Parameter를 설정해준다.
@Parameter static var isFirstLaunch: Bool = true
// 중략...
TipView(swiftDataTip, arrowEdge: .bottom)
.padding(.horizontal)
// 팁이 버튼 위에 표시되도록 설정
SwiftDataButton()
// 중략...
TipView(daliyQuizTip, arrowEdge: .bottom)
.padding(.horizontal)
DailyQuizButton()
마지막으로 사용자가 앱을 최초 사용하는 것이 맞는지 판단하고 결과에 따라 @Parameter값을 변경시키도록 한다.
앱을 처음 사용하는 것인지 판단하기 위해 UserDefaults를 사용한다.
.onAppear() {
// 뷰가 생성되면 이벤트를 발생 -> 이벤트 횟수 증가
Task {
await SwiftDataTips.swiftDataTip.donate()
await DailyQuizTips.dailyQuizTip.donate()
}
// 만약 이벤트 1 혹은 이벤트 2의 이벤트 발생 횟수가 1보다 크다면?
if SwiftDataTips.swiftDataTip.donations.count > 1 || DailyQuizTips.dailyQuizTip.donations.count > 1 {
// @Parameter 값을 false로 변경 -> 팁이 더이상 표시되지 않음
MainTitleView.isFirstLaunch = false
// @Parameter 값을 UserDefaults에 저장
UserDefaults.standard.set(MainTitleView.isFirstLaunch, forKey: "userFirstLaunch")
// 1보다 작을 경우
} else {
// @Parameter 값은 true
MainTitleView.isFirstLaunch = true
// @Parameter 값을 UserDefaults에 저장
UserDefaults.standard.set(MainTitleView.isFirstLaunch, forKey: "userFirstLaunch")
}
// 뷰가 생성될 때 @Parameter 값은 UserDefaults값을 가져온다
// 2번째 방문일 경우 false가 되어 팁이 더이상 표시되지 않음
MainTitleView.isFirstLaunch = UserDefaults.standard.bool(forKey: "userFirstLaunch")
}

오늘은 TipKit을 활용하여 앱에 Tip을 표시하는 방법에 대해 학습했다.
간단하게 하려면 간단하게 구현할 수 있지만, 조건을 추가하려고 하면 한없이 복잡해져서 익히는데 어려움이 있었다.
어떻게 하면 팁을 더욱 보기 편하고 사용자에게 도움이 되게 구현할 수 있을지 고민이 필요할 것 같다.