This project is intended for use by 2023 Grace Hopper Conference Attendees attending SESS - 1434 - Building a robust mobile app: From concept to app.
Skills
- Xcode
- Swift
- Git
Attendess are welcomed to participate in the session if they just choose to listen and follow along via presentation.
Optional
- Macbook
- Minimum Xcode 14 installed
During the lab we will build an app for conference attendees. The app will let attendess view conference sessions, register for sessions, and view their schedule.
The lab is organized into steps. We will be building the app in steps. Steps are provided via tags on the project. If ever attendees get lost or have trouble keeping up, you can clone a particular tag to keep up with the session.
git clone -b step-1 https://github.com/usaa/ghc-2023-conference-app.git
- Step 1 - Create Initial Conference Schedule View
- Step 2 - MVVM - Create Conference Schedule Model and View Model
- Step 3 - MVVM - Create My Schedule View and View Model
- Step 4 - MVVM - Expand on our Conference Schedule Model
- Step 5 - Add Tab View, Add indicator for registration, Mock registration service
- Step 6 - Expand on the mocked service
- Step 7 - Repository - Create a repository which manages data for both views
- Step 8 - Reactive Programming - Conference Schedule View observation on Conference Schedule Result
- Step 9 - Reactive Programming - My Schedule View observation on Conference Schedule Result
- Step 10 - Reactive Programming - Updates to Conference Schedule
- Step 11 - Caching
- Step 12 - Performance in Caching
- Step 13 - Dependency Injection
- Step 14 - Dependency Injection - Unit Tests
- Step 15 - SwiftUI - UI Enhancements to the App
Modify isRegistered Bool
func register(session: Session) -> ConferenceSchedule {
if let s = self.conferenceSchedule.sessions?.firstIndex(of: session) {
var registerSession = session
registerSession.isRegistered = true
self.conferenceSchedule.sessions?[s] = registerSession
}
return self.conferenceSchedule
}
func unregister(session: Session) -> ConferenceSchedule {
if let s = self.conferenceSchedule.sessions?.firstIndex(of: session) {
var registerSession = session
registerSession.isRegistered = false
self.conferenceSchedule.sessions?[s] = registerSession
}
return self.conferenceSchedule
}
Persist conferenceSchedule
in register and unregister functions.
guard let encodedData = try? JSONEncoder().encode(self.conferenceSchedule) else {
return self.conferenceSchedule
}
UserDefaults.standard.set(encodedData, forKey: "ServiceConferenceSchedule")
return self.conferenceSchedule
Access the persisted data from getSchedule
func getSchedule() -> ConferenceSchedule {
if let storedData = UserDefaults.standard.data(forKey: "ServiceConferenceSchedule"),
let data = try? JSONDecoder().decode(ConferenceSchedule.self, from: storedData) {
self.conferenceSchedule = data
return data
}
return self.conferenceSchedule
}
ConferenceScheduleResult enum
enum ConferenceScheduleResult {
case uninitialized
case success(data: ConferenceSchedule)
var data: ConferenceSchedule {
switch self {
case .uninitialized:
return ConferenceSchedule(userID: 98765, sessions: [])
case let .success(data):
return data
}
}
}
Set up view model to subscribe on data
let result = self.repository.data.map { $0 }
result
.receive(on: DispatchQueue.main)
.sink {
self.result = $0
}
.store(in: &cancellables)
Set up view model to subscribe on data
let data = repository.data.map { $0.data }
data
.receive(on: DispatchQueue.main)
.sink {
self.result = .success(data: $0)
}
.store(in: &cancellables)
Caching
var minutesToLive: Double
private var secondsToLive: TimeInterval {
return minutesToLive * 60
}
var expired: Bool { return dataAge + secondsToLive < currentTime() }
var dateCached: Date? {
if dataAge == 0 {
return nil
} else {
return Date(timeIntervalSince1970: dataAge)
}
}
var dataAge: TimeInterval = 0
/// Returns the current time in seconds, used to determine lifespan of data
var currentTime: () -> TimeInterval = { Date().timeIntervalSince1970 }
public init(minutesToLive: Double = 5.0) {
self.minutesToLive = minutesToLive
if let storedTime = UserDefaults.standard.data(forKey: "ConferenceScheduleStorageTime"),
let storageTime = try? JSONDecoder().decode(StorageTime.self, from: storedTime) {
dataAge = storageTime.storageTimeInterval
if let storedData = UserDefaults.standard.data(forKey: "ConferenceSchedule"),
let cache = try? JSONDecoder().decode(ConferenceSchedule.self, from: storedData) {
if expired {
clear()
} else {
put(.success(data:cache))
}
}
}
}
func put(_ result: ConferenceScheduleResult) {
switch(result) {
case .uninitialized:
dataAge = 0
default:
dataAge = currentTime()
}
self.persist(schedule: result)
currentValue = result
}
func clear() {
dataAge = 0
currentValue = ConferenceScheduleResult.uninitialized
}
private func persist(schedule: ConferenceScheduleResult) {
//persist data
let data = schedule.data
guard let encodedData = try? JSONEncoder().encode(data) else { return }
UserDefaults.standard.set(encodedData, forKey: "ConferenceSchedule")
//persist time of data
guard let encodedStorageTime = try? JSONEncoder().encode(StorageTime(timeInterval: currentTime())) else { return }
UserDefaults.standard.set(encodedStorageTime, forKey: "ConferenceScheduleStorageTime")
}
}
class StorageTime: Codable {
private var storageTime: Double
var storageTimeInterval: TimeInterval {
return storageTime / 1000
}
init(timeInterval: TimeInterval) {
self.storageTime = timeInterval * 1000
}
}
Refresh function
public func refresh() {
//let loadingResult: ConferenceScheduleResult = .loading
//store.put(loadingResult)
Task {
try await Task.sleep(nanoseconds: 2_000_000_000)
let result = service.getSchedule()
store.put(result)
}
}
DataStore protocol
protocol DataStore {
var data: AnyPublisher<ConferenceScheduleResult, Never> { get }
var currentValue: ConferenceScheduleResult { get }
var minutesToLive: Double { get }
var expired: Bool { get }
var dateCached: Date? { get }
func put(_ result: ConferenceScheduleResult)
func clear()
}
Dependency Initialization
let service = ConferenceScheduleService()
let store = ConferenceScheduleDataStore(minutesToLive: 0.5)
let conferenceScheduleRepository: ConferenceScheduleRepository
let conferenceScheduleViewModel: ConferenceScheduleViewModel
let myScheduleViewModel: MyScheduleViewModel
init() {
self.conferenceScheduleRepository = ConferenceScheduleRepository(service: self.service, store: self.store)
self.conferenceScheduleViewModel = ConferenceScheduleViewModel(repository: self.conferenceScheduleRepository)
self.myScheduleViewModel = MyScheduleViewModel(repository: self.conferenceScheduleRepository)
}
Ashley Pham and Jennifer Kartchner
This project is Open Source software released under the Apache 2.0 license
USAA reserves the right to remove the contents of our github at any time, without notice.