Skip to content

Commit

Permalink
Merge pull request #1110 from DroidKaigi/ios-add-dynamic-links-nfc
Browse files Browse the repository at this point in the history
[iOS] Support Achievement Rally with NFC
  • Loading branch information
ry-itto authored Sep 9, 2023
2 parents 92bd03a + 27b4f65 commit 5835b6e
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 62 deletions.
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()
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
68 changes: 68 additions & 0 deletions app-ios/Modules/Sources/Achievements/AchievementsViewModel.swift
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

0 comments on commit 5835b6e

Please sign in to comment.