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 all 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
4 changes: 3 additions & 1 deletion app-ios/App/DroidKaigi2023/DroidKaigi2023/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Theme

@main
struct MainApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

init() { FontAssets.registerAllCustomFonts() }

var body: some Scene {
WindowGroup {
RootView()
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,18 @@
<!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>
<string>applinks:droidkaigiapp.page.link</string>
</array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
18 changes: 18 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,23 @@
<false/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>NFCReaderUsageDescription</key>
<string>Achievement の読み取りに使用します。</string>
<key>FirebaseDynamicLinksCustomDomains</key>
<array>
<string>https://confsched2023.page.link</string>
<string>https://droidkaigiapp.page.link</string>
</array>
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>io.github.droidkaigi.DroidKaigi2023</string>
</array>
</dict>
</array>
</dict>
</plist>
23 changes: 23 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 @@ -98,6 +99,16 @@ var package = Package(
]
),

.target(
name: "DeepLink",
dependencies: [
"KMPContainer",
"shared",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "FirebaseDynamicLinks", package: "firebase-ios-sdk"),
]
),

.target(
name: "Event",
dependencies: [
Expand Down Expand Up @@ -194,9 +205,12 @@ var package = Package(
name: "Achievements",
dependencies: [
"Assets",
"DeepLink",
"Theme",
"KMPContainer",
"NFC",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
]
),
.testTarget(
Expand Down Expand Up @@ -230,12 +244,21 @@ var package = Package(
"Achievements",
"Assets",
"Contributor",
"DeepLink",
"FloorMap",
"Session",
"Sponsor",
"Staff",
"Theme",
"Timetable",
.product(name: "FirebaseRemoteConfig", package: "firebase-ios-sdk"),
]
),

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

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,69 @@
import AsyncAlgorithms
import CryptoKit
import DeepLink
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 deepLink = DeepLink()
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 {
let readed = try await nfcReader.read()
if
let urlString = readed,
let url = URL(string: urlString) {
if let dynamicLink = try await deepLink.dynamicLink(shortLink: url) {
return try await deepLink.handleDynamicLink(dynamicLink: dynamicLink)
}
}
} catch {
print(error)
}
return nil
}
}
Loading
Loading