-Preview-

Color App TipparSta! Tip

-Project progress-

  • SplashView 구현
  • CustomTabView 구현
  • SettingView 구현
  • HomeView 구현
  • AccountView 구현
  • SwiftDataView 구현
  • DailyQuizView 구현
  • RankSystem 구현

디테일업 및 추가기능 구현 중


-View made today-

Color App Tip ViewparSta! Tip View
TipKit을 연습프로젝트에 적용하기

-View Review-

1. Color App Tip View

오늘은 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")
    }
}

1) 랜덤한 ColorBlock을 생성하기

이번에는 앱에서 기본적으로 컬러블록을 몇가지 제공하고, 트리거를 통해 사용자가 랜덤한 컬러의 컬러블록을 생성할 수 있도록 한다.
이를 위해 먼저 랜덤한 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에 대한 정의와 선언을 해준다.

2) 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 - 뷰의 위에 표시된다.
InlineTipPopoverTip

이번에는 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()
}

3) Tip Rule 사용하기

팁은 사용자에게 앱을 사용하는데 도움을 주지만, 항상 표시가 된다면 오히려 불편을 줄 수 있다.
때문에 필요할 때만 표시되거나 특정 트리거에 의할 때만 팁이 표시되어야 한다.
SwiftUI에서는 Tip 프로토콜의 Rule을 통해 팁이 생성되는 이벤트를 설정할 수 있도록 도와준다.

이번에는 Help 버튼을 만들어 버튼을 누르면 앱 사용 팁이 나오도록 한다.
Rule을 사용하는 방법은 2가지가 있다.

    1. @Parameter 사용하기
    1. Event 사용하기

a. 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

그리고 onAppearDispatchQueue를 활용하여 뷰가 생성되고 일정 시간이 경과했을 때 실행할 액션을 설정해준다.

.onAppear() {
	// 뷰가 생성되고 5초가 지났을 때
	DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    	// 사용자가 버튼을 클릭했는가?
    	guard self.buttonClicked == false else {
        	return
		}
        // 클릭하지 않았다면 팁을 표시
		self.buttonClicked.toggle()
        ContentView.helpCall.toggle()
	}
}
구현 결과

b. Event 사용하기

앱을 사용하면서 한 번 이상 발생할 수 있는 이벤트(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()
			}
		}
	}
구현 결과

4) 구현 결과물


2. parSta! Tip View

오늘 학습한 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")
}

1) Tip 정의하기(SwiftDataTips, DailyQuizTips)

우선 사용할 팁을 정의한다. 이번에는 SwiftDataDailyQuiz의 사용법을 알려주는 팁만 추가하려고 한다.

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
        }
    }
}

2) Tip 선언 및 조건 설정

메인 뷰로 돌아와 TipView를 선언해주고, Rule로 지정해둔 @Parameter를 설정해준다.

@Parameter static var isFirstLaunch: Bool = true
// 중략...
TipView(swiftDataTip, arrowEdge: .bottom)
	.padding(.horizontal)
// 팁이 버튼 위에 표시되도록 설정
SwiftDataButton()
// 중략...
TipView(daliyQuizTip, arrowEdge: .bottom)
	.padding(.horizontal)
DailyQuizButton()

3) onAppear 설정

마지막으로 사용자가 앱을 최초 사용하는 것이 맞는지 판단하고 결과에 따라 @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")
}

5) 구현 결과물

-Today's lesson review-

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

0개의 댓글