Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Support Achievement Rally with NFC #1110

Merged
merged 11 commits into from
Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@
"version" : "1.2.3"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms",
"state" : {
"revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a",
"version" : "0.1.0"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
Expand All @@ -171,6 +180,15 @@
"version" : "1.0.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x50",
"green" : "0x6C",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xB0",
"green" : "0xDC",
"red" : "0x61"
}
},
"idiom" : "universal"
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:confsched2023.page.link</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
</dict>
</plist>
2 changes: 2 additions & 0 deletions app-ios/App/DroidKaigi2023/DroidKaigi2023/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<false/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>NFCReaderUsageDescription</key>
<string>Achievement の読み取りに使用します。</string>
</dict>
</plist>
10 changes: 10 additions & 0 deletions app-ios/Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var package = Package(
.package(url: "https://github.com/cybozu/LicenseList", from: "0.2.1"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.14.0"),
.package(url: "https://github.com/airbnb/lottie-spm", from: "4.2.0"),
.package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -196,7 +197,9 @@ var package = Package(
"Assets",
"Theme",
"KMPContainer",
"NFC",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
]
),
.testTarget(
Expand Down Expand Up @@ -239,6 +242,13 @@ var package = Package(
]
),

.target(
name: "NFC",
dependencies: [
"Model",
]
),

.target(
name: "RemoteConfig",
dependencies: [
Expand Down
124 changes: 108 additions & 16 deletions app-ios/Modules/Sources/Achievements/AchievementsView.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,121 @@
import Assets
import shared
import SwiftUI
import Theme

public struct AchievementsView: View {
@ObservedObject var viewModel: AchievementsViewModel = .init()
@State private var obtainedAchievement: Achievement?

public init() {}

public var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
Text("会場の各部屋に設置されたNFCタグにスマホをかざしてバッジを集めてみましょう。イベント最終日には、全てのバッジを集めた方にDroidKaigiのオリジナルグッズをプレゼントします。")
.font(Font.system(size: 16))
.foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor)
LazyVGrid(columns: [.init(), .init()]) {
Assets.Images.achievementArcticFox.swiftUIImage
Assets.Images.achievementBumblebee.swiftUIImage
Assets.Images.achievementChipmunk.swiftUIImage
Assets.Images.achievementDolphin.swiftUIImage
// TODO: Find good render way
Assets.Images.achievementElectricEel.swiftUIImage
switch viewModel.state.loadedState {
case .initial, .loading:
ProgressView()
.task {
await viewModel.load()
}
case .failed:
EmptyView()
case .loaded(let state):
NavigationStack {
ScrollView {
ZStack {
VStack(spacing: 24) {
Text(state.description)
.font(Font.system(size: 16))
.foregroundStyle(AssetColors.Surface.onSurfaceVariant.swiftUIColor)
LazyVGrid(columns: [.init(), .init()]) {
AchievementImage(
target: Achievement.arcticfox,
savedAchievements: state.achievements,
activeImage: Assets.Images.achievementArcticFoxActive,
inactiveImage: Assets.Images.achievementArcticFox
)
AchievementImage(
target: Achievement.bumblebee,
savedAchievements: state.achievements,
activeImage: Assets.Images.achievementBumbleBeeActive,
inactiveImage: Assets.Images.achievementBumblebee
)
AchievementImage(
target: Achievement.chipmunk,
savedAchievements: state.achievements,
activeImage: Assets.Images.achievementChipmunkActive,
inactiveImage: Assets.Images.achievementChipmunk
)
AchievementImage(
target: Achievement.dolphin,
savedAchievements: state.achievements,
activeImage: Assets.Images.achievementDolphinActive,
inactiveImage: Assets.Images.achievementDolphin
)
// TODO: Find good render way
AchievementImage(
target: Achievement.electriceel,
savedAchievements: state.achievements,
activeImage: Assets.Images.achievementElectricEelActive,
inactiveImage: Assets.Images.achievementElectricEel
)
}
Button {
Task {
let result = await viewModel.read()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, we need an action just before reading NFC tags 👀

obtainedAchievement = result
}
} label: {
Text("Scan NFC Tag")
}
.buttonStyle(.bordered)
.controlSize(.large)
}
.padding(16)
if let achievement = obtainedAchievement?.toLottie() {
AssetColors.Custom.black.swiftUIColor.opacity(0.84)
achievement.swiftUIAnimation(loopMode: .playOnce) { _ in
obtainedAchievement = nil
}
}
}
}
.padding(16)
.background(AssetColors.Surface.surface.swiftUIColor)
.navigationTitle("Achievements")
}
.background(AssetColors.Surface.surface.swiftUIColor)
.navigationTitle("Achievements")
}
}
}

struct AchievementImage: View {
let target: Achievement
let savedAchievements: Set<Achievement>
let activeImage: ImageAsset
let inactiveImage: ImageAsset

var body: some View {
if savedAchievements.contains(target) {
activeImage.swiftUIImage
} else {
inactiveImage.swiftUIImage
}
}
}

private extension Achievement {
func toLottie() -> LottieAnimation? {
switch self {
case .arcticfox:
return LottieAssets.achievementAJson
case .bumblebee:
return LottieAssets.achievementBJson
case .chipmunk:
return LottieAssets.achievementCJson
case .dolphin:
return LottieAssets.achievementDJson
case .electriceel:
return LottieAssets.achievementEJson
default:
return nil
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
import AsyncAlgorithms
import CryptoKit
import Dependencies
import Foundation
import KMPContainer
import Model
import NFC
import shared

struct AchievementsViewState: ViewModelState {
struct LoadedState: Equatable {
var description: String
var achievements: Set<Achievement>
}

var loadedState: LoadingState<LoadedState> = .initial
}

@MainActor
class AchievementsViewModel: ObservableObject {
@Dependency(\.achievementData) var achievementData
@Published var state: AchievementsViewState = .init()
private let nfcReader = NFCReader()
private var loadTask: Task<Void, Error>?

deinit {
loadTask?.cancel()
}

func load() async {
state.loadedState = .loading

loadTask = Task.detached { @MainActor in
do {
for try await (description, achievements) in combineLatest(
self.achievementData.achievementDetailDescription(),
self.achievementData.achievements()
) {
self.state.loadedState = .loaded(
.init(
description: description,
achievements: achievements
)
)
}
} catch {
self.state.loadedState = .failed(error)
}
}
}

func read() async -> Achievement? {
do {
if let urlString = try await nfcReader.read(), let url = URL(string: urlString) {
let hashedString = idToSha256(id: url.lastPathComponent)

let target: Achievement? = switch hashedString {
case Achievement.arcticfox.sha256:
Achievement.arcticfox
case Achievement.bumblebee.sha256:
Achievement.bumblebee
case Achievement.chipmunk.sha256:
Achievement.chipmunk
case Achievement.dolphin.sha256:
Achievement.dolphin
case Achievement.electriceel.sha256:
Achievement.electriceel
default:
nil
}

if let achievement = target {
try await achievementData.saveAchievement(achievement)
return achievement
}
}
} catch {
print(error)
}
return nil
}

private func idToSha256(id: String) -> String? {
guard let data = id.data(using: .utf8) else {
return nil
}
let digest = SHA256.hash(data: data)
return digest.compactMap { String(format: "%02x", $0) }.joined()
}
}
2 changes: 2 additions & 0 deletions app-ios/Modules/Sources/Assets/LottieView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Lottie
import SwiftUI

public typealias LottieAnimation = Lottie.LottieAnimation

public struct LottieView: UIViewRepresentable {
private let animation: LottieAnimation
private let loopMode: LottieLoopMode
Expand Down
Loading
Loading