so here's my issue with LiveActivities. My code doesn't work, but no error is thrown...
Here's how I enable/disable the activity:
let helper = JourneyActivityHelper.shared
TagComponent(journey: journey)
.onChange(of: journey.isTracked) { value in
journey.isTracked ? helper.start(departureTime: departureTime, arrivalTime: arrivalTime, journey: journey) : helper.stop(departureTime: departureTime, arrivalTime: arrivalTime)
}
Here's my helper class:
struct JourneyAttributes: ActivityAttributes {
public typealias JourneyStatus = ContentState
public struct ContentState: Codable, Hashable {
var departureTime: Date
var arrivalTime: Date
}
var journey: Journey
}
class JourneyActivityHelper {
static let shared = JourneyActivityHelper()
var activity: Activity<JourneyAttributes>?
func start(departureTime: Date, arrivalTime: Date, journey: Journey) {
// We check that activities can be enabled
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
// Initializing variables
let initialState = JourneyAttributes.ContentState(departureTime: departureTime, arrivalTime: arrivalTime)
let staleDate = departureTime <= Date() ? nil : departureTime
let initialContent = ActivityContent(state: initialState, staleDate: staleDate)
let attributes = JourneyAttributes(journey: journey)
do {
activity = try Activity.request(attributes: attributes, content: initialContent)
print("New live activity activated!")
} catch (let error) {
print(error.localizedDescription)
}
}
func stop(departureTime: Date, arrivalTime: Date) {
Task {
let finalState = JourneyAttributes.ContentState(departureTime: departureTime, arrivalTime: arrivalTime)
let finalContent = ActivityContent(state: finalState, staleDate: nil)
for activity in Activity<JourneyAttributes>.activities {
await activity.end(finalContent, dismissalPolicy: .immediate)
}
}
}
}
And finally, here's my LiveActivity code (Widget):
#main
struct TrainlyWidgetBundle: WidgetBundle {
#WidgetBundleBuilder
var body: some Widget {
TrainlyWidget()
if #available(iOS 16.2, *) {
ActivityConfiguration(for: JourneyAttributes.self) { context in
LockScreenLiveActivity(context: context)
} dynamicIsland: { context in
...
}
}
}
}
Thanks in advance for anyone who could help me resolve my issue!
EDIT: What I expected: for my LiveActivity to show up. What happens: it doesn't show up!
I solved my own issue. departureTime and arrivalTime (dates that were passed to start() and stop() functions) weren't actual dates. Therefore, the Live Activity didn't start because the dates were in the past (year 2000 instead of 2023)
Related
In the Home Screen of my app, I have a Capsule() that contains the user's steps. This data is obtained via HealthKit. The data is correct however when it changes in the health app, it should change in my app but this is not happening. How do I get 'steps' variable to listen to HealthKit, there must be an error in my code.
Here is the code for my Home view:
import SwiftUI
import HealthKit
struct Home: View {
#ObservedObject var healthStore = HealthStore()
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let startDate = Date()
let endDate = Date()
statisticsCollection.enumerateStatistics(from: startDate, to: endDate) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate)
steps.append(step)
}
}
var body: some View {
NavigationView {
ScrollView {
ZStack {
Color("BackgroundColour")
.ignoresSafeArea()
VStack {
let totalSteps = steps.reduce(0) { $0 + $1.count }
ForEach($steps, id: \.id) { step in
Button(action: {
// Perform button action here
print("Step Capsule Tapped...")
}) {
HStack {
Image("footsteps")
Text("\(totalSteps)")
}
}
} // ForEach End
} // VStack End
}//ZStack End
.edgesIgnoringSafeArea(.all)
} // ScrollView End
.background(Color("BackgroundColour"))
.onLoad {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateSteps { statisticsCollection in
if let statisticsCollection = statisticsCollection {
// update the UI
updateUIFromStatistics(statisticsCollection)
}
}
}
}
} // .onLoad End
.onAppear(perform: {
let defaults = UserDefaults.standard
let keyString: String? = defaults.string(forKey: "key") ?? ""
print("User's Key:\(keyString ?? "")")
}) // .onAppear End
} // NavigationView End
}
}
Here is the code for the HealthStore:
import Foundation
import HealthKit
import SwiftUI
import Combine
class HealthStore: ObservableObject {
#Published var healthStore: HKHealthStore?
#Published var query: HKStatisticsCollectionQuery?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateSteps(completion: #escaping (HKStatisticsCollection?)-> Void) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let anchorDate = Date.mondayAt12AM()
let daily = DateComponents(day: 1)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates:
[.init(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered), predicate]
)
query = HKStatisticsCollectionQuery(
quantityType: stepType,
quantitySamplePredicate: compoundPredicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: daily)
query!.initialResultsHandler = { query, statisticsCollection, error in
completion(statisticsCollection)
}
if let healthStore = healthStore, let query = self.query {
healthStore.execute(query)
}
}
func requestAuthorization(completion: #escaping (Bool) -> Void) {
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
guard let healthStore = self.healthStore else { return completion(false) }
healthStore.requestAuthorization(toShare: [], read: [stepType]) { (success, error) in
completion(success)
}
}
}
extension Date {
static func mondayAt12AM() -> Date {
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))!
}
}
Here is the code for Step:
import Foundation
struct Step: Identifiable {
let id = UUID()
let count: Int
let date: Date
}
Here is the code for the .onLoad method that is used on the Home view:
import SwiftUI
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Any ideas?
I think I know what's missing here.
In your class HealthStore, inside the method func calculateSteps(completion: #escaping (HKStatisticsCollection?)-> Void), you have set the initialResultsHandler property of your HKStatisticsCollectionQuery. However, you've not set the statisticsUpdateHandler property.
To continue to listen to updates from HealthKit, after initialResultsHandler completes execution, you need to also set statisticsUpdateHandler.
As-per the statisticsUpdateHandler documentation, in-case statisticsUpdateHandler is nil, which is the case above, the HKStatisticsCollectionQuery will:
"automatically stop as soon as it has finished calculating the initial results"
which seems to be in-line with what you are observing.
Full disclosure, I've never worked with HealthKit, so please pardon me if this doesn't help. I've gone through the HealthKit documentation after looking at your code and writing here what jumps out at me.
If it does help you though, kindly consider marking it as the answer.
Creating an example for a struct is very easy and straightforward. For example,
import Foundation
struct User: Identifiable, Codable {
let id: UUID
let isActive: Bool
let name: String
let age: Int
let company: String
static let example = User(id: UUID(), isActive: true, name: "Rick Owens", age: 35, company: "Rick Owens Inc.")
}
Now, how can I create an example if I made this an entity in core data? I can't just put let example = CachedUser(id: UUID(), ...) like I did with the struct. I want this example to be part of my core data automatically without having to manually create it by using forms, buttons, etc... Thanks in advance!
You can simply check if your default user exists in database. If it does not then you need to create one and save it. Something like the following would work if you have synchronous operations:
class CachedUser {
static var example: CachedUser = {
let exampleUUID = UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!
if let existingUser = Database.fetchUser(id: exampleUUID) {
return existingUser
} else {
let newUser = CachedUser()
// TODO: apply example values to user
Database.saveUser(newUser)
return newUser
}
}()
}
This will lazily return existing or generate a new user for you. This user will then be persistent in your database.
The code will only be executed once per session, first time you call CachedUser.example.
If you have your database setup asynchronous then with closures it should look something like this:
class User {
static private(set) var example: User!
static func prepareExampleUser(_ completion: () -> Void) {
let exampleUUID = UUID(uuidString: "33041937-05b2-464a-98ad-3910cbe0d09e")!
Database.fetchUser(id: exampleUUID) { user in
if let user = user {
example = user
completion()
} else {
let newUser = User()
newUser.id = exampleUUID
// TODO: apply example values to user
Database.saveUser(newUser) {
example = newUser
completion()
}
}
}
}
But in this case it makes sense to warmup your application before you show screens that require this user to be present. You can for instance have a loading screen when your app first starts and continue to next screen once this method has finished...
// Loading screen enters
self.startLoading()
User.prepareExampleUser {
self.navigateToNextScreen()
self.stopLoading()
}
In both cases you now hold a static property to your example entry such as User.example which can be used anywhere.
But in both cases you may stumble to issue if user (if able to) deletes this entry from database. You would need to handle that case. Either prevent that action or create a new example user once the old one is deleted.
To access this manager put
let mgr = CachedUserPersistenceManager()
In a ViewModel or a View
/// Manager for the Item entity
class CachedUserPersistenceManager: PersistenceManager<CachedUser>{
let sampleUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
init(isTest: Bool = false) {
super.init(entityType: CachedUser.self, isTest: isTest)
//Preloads the user
preloadSample()
}
///Preloads a sample object to the context
func preloadSample(){
let list = retrieveObjects(sortDescriptors: nil, predicate: NSPredicate(format: "%K == %#", #keyPath(CachedUser.uuid), sampleUUID as CVarArg)
)
if list.isEmpty{
let sampleItem = createObject()
sampleItem.uuid = sampleUUID
save()
}
}
override func addSample() -> CachedUser {
let new = super.addSample() as CachedUser
//add any sample code
return new
}
override func createObject() -> CachedUser {
super.createObject()!
}
override func updateObject(object: CachedUser) -> Bool {
//Replace the uuid if needed
if object.uuid == sampleUUID{
object.uuid = UUID()
}
return super.updateObject(object: object)
}
}
The generic classes that are a part of this code are below. You don't need them per say it just makes some of the code reusable through the app.
//Manager for any Entity
class PersistenceManager<T : NSManagedObject>{
let serviceSD: CoreDataPersistenceService<T>
internal init(entityType: T.Type, isTest: Bool = false) {
self.serviceSD = CoreDataPersistenceService(isTest: isTest, entityType: entityType)
}
//MARK: convenience
func addSample() -> T {
let newItem = createObject()
return newItem!
}
//MARK: Persistence Service Methods
func createObject() -> T? {
let result = serviceSD.createObject()
return result
}
func updateObject(object: T) -> Bool {
return serviceSD.updateObject(object: object)
}
func deleteObject(object: T) -> Bool {
return serviceSD.deleteObject(object: object)
}
func deleteAllObjects(entityName: String, isConfirmed: Bool) -> Bool {
return serviceSD.deleteAllObjects(isConfirmed: isConfirmed)
}
func retrieveObjects(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?) -> [T]{
return serviceSD.retrieveObjects(sortDescriptors: sortDescriptors, predicate: predicate)
}
func retrieveObject(id: String) -> T? {
return serviceSD.retrieveObject(sortDescriptors: nil, id: id).first
}
func resetChanges() {
serviceSD.resetChanges()
}
func save() {
_ = serviceSD.save()
}
}
//Service for Any Entity
class CoreDataPersistenceService<T: NSManagedObject>: NSObject {
var persistenceController: PersistenceController
let entityType: T.Type
required init(isTest: Bool = false, entityType: T.Type) {
if isTest{
self.persistenceController = PersistenceController.preview
}else{
self.persistenceController = PersistenceController.previewAware
}
self.entityType = entityType
super.init()
}
//MARK: CRUD methods
func createObject() -> T? {
let result = entityType.init(context: persistenceController.container.viewContext)
return result
}
func updateObject(object: T) -> Bool {
var result = false
result = save()
return result
}
func deleteObject(object: T) -> Bool {
var result = false
persistenceController.container.viewContext.delete(object)
result = save()
return result
}
func deleteAllObjects(isConfirmed: Bool) -> Bool {
var result = false
//Locked in so only the Generic "Item" can be deleted like this
if entityType == Item.self && isConfirmed == true{
let deleteRequest = NSBatchDeleteRequest(fetchRequest: entityType.fetchRequest())
do {
try persistenceController.container.persistentStoreCoordinator.execute(deleteRequest, with: persistenceController.container.viewContext)
} catch {
print(error)
result = false
}
}
return result
}
func resetChanges() {
persistenceController.container.viewContext.rollback()
_ = save()
}
func save() -> Bool {
var result = false
do {
if persistenceController.container.viewContext.hasChanges{
try persistenceController.container.viewContext.save()
result = true
}else{
result = false
}
} catch {
print(error)
}
return result
}
func retrieveObject(sortDescriptors: [NSSortDescriptor]? = nil, id: String) -> [T]{
return retrieveObjects(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: "id == %#", id))
}
func retrieveObjects(sortDescriptors: [NSSortDescriptor]? = nil, predicate: NSPredicate? = nil) -> [T]
{
let request = entityType.fetchRequest()
if let sortDescriptor = sortDescriptors
{
request.sortDescriptors = sortDescriptor
}
if let predicate = predicate
{
request.predicate = predicate
}
do
{
let results = try persistenceController.container.viewContext.fetch(request)
return results as! [T]
}
catch
{
print(error)
return []
}
}
}
The previewAware variable that is mentioned goes with the Apple standard code in the PersistenceController
It automatically give you the preview container so you don't have to worry about adapting your code for samples in Canvas. Just add the below code to the PersistenceController
static var previewAware : PersistenceController{
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
return PersistenceController.preview
}else{
return PersistenceController.shared
}
}
I'm building a SwiftUI app that retrieves an array of movies by genre from The Movie Database API.
Once the user selects a movie, I make a second API to get details for that specific movie. I'm using #Published to notify the view of changes however I am getting the I get the error "Missing argument for parameter 'from' in call" whenever I call an instance of the Model.
Here's the Model:
import Foundation
// MARK: - MovieList
struct MovieList: Codable {
let page: Int
let totalResults: Int
let totalPages: Int
let movie: [Movie]
enum CodingKeys: String, CodingKey {
case page
case totalResults = "total_results"
case totalPages = "total_pages"
case movie = "results"
}
}
// MARK: - Movie
struct Movie: Codable {
let popularity: Double
let voteCount: Int
let video: Bool
let posterPath: String?
let id: Int
let adult: Bool
let backdropPath: String?
let title: String
let voteAverage: Double
let overview: String
let releaseDate: String?
let runTime: Int?
enum CodingKeys: String, CodingKey {
case popularity
case voteCount = "vote_count"
case video
case posterPath = "poster_path"
case id, adult
case backdropPath = "backdrop_path"
case title
case voteAverage = "vote_average"
case overview
case releaseDate = "release_date"
case runTime = "runtime"
}
}
And here's the View Model:
import Foundation
class DetailViewModel: ObservableObject {
#Published var fetchedMovie = Movie() // getting error here
func getMovieDetails(id: Int) {
WebService().getMovieDetails(movie: id) { movie in
if let movieDetails = movie {
self.fetchedMovie = movieDetails
}
}
}
}
And here's the network call:
func getMovieDetails(movie: Int, completion: #escaping (Movie?) -> ()) {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/\(movie)?api_key=5228bff935f7bd2b18c04fc3633828c0") else {
fatalError("Invalid URL")
}
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: url) { data, response, error in
// Check for errors
guard error == nil else {
print ("error: \(error!)")
return
}
// Check that data has been returned
guard let data = data else {
print("No data")
return
}
do {
let decoder = JSONDecoder()
let movieDetails = try decoder.decode(Movie.self, from: data)
DispatchQueue.main.async {
completion(movieDetails)
}
} catch let err {
print("Err", err)
}
}
// execute the HTTP request
task.resume()
}
}
And the View code:
import SwiftUI
struct MovieDetailView: View {
#ObservedObject private var detailVM = DetailViewModel() // error here: Missing argument for parameter 'movie' in call
var movie: DetailViewModel
var body: some View {
VStack {
URLImage(url: "\(movie.backdropURL)")
.aspectRatio(contentMode: .fit)
Text("\(detailVM.movieRunTime) mins")
Text(movie.movieOverview)
.padding()
Spacer()
}.onAppear {
self.detailVM.getMovieDetails(id: self.movie.id)
}
.navigationBarTitle(movie.movieTitle)
}
}
struct MovieDetailView_Previews: PreviewProvider {
static var previews: some View {
MovieDetailView(movie: DetailViewModel(movie: Movie.example))
}
}
Any help would be greatly appreciated.
You cant initialise Movie object like this ... it needs Decoder object or all member wise intialization ---
You can define your function like this
class DetailViewModel: ObservableObject {
#Published var fetchedMovie : Movie?
func getMovieDetails(id: Int) {
WebService().getMovieDetails(movie: id) { movie in
if let movieDetails = movie {
self.fetchedMovie = movieDetails
}
}
}
}
I have a list
List {
ForEach (appState.foo.indices, id: \.self) { fooIndex in
Text(foo[fooIndex].name)
}
.onDelete(perform: self.deleteRow)
}
with a function that deletes a row from the foo array:
private func deleteRow(at indexSet: IndexSet) {
self.appState.foo.remove(atOffsets: indexSet)
}
and an observable object that acts as an environment object in the view with the list:
class AppState: ObservableObject {
var foo: [Bar] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "foo")
}
objectWillChange.send()
self.myFunc()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
// ....
func myFunc() {
print("I'm not printing when you delete a row")
}
}
How can I get my myFunc() triggered when I delete a row?
Use stored property instead of a computed property. To fix your issue modify the foo property in AppState like this:
struct Bar: Encodable { }
class AppState: ObservableObject {
var foo: [Bar] {
didSet {
if let encoded = try? JSONEncoder().encode(foo) {
UserDefaults.standard.set(encoded, forKey: "foo")
}
objectWillChange.send()
myFunc()
}
}
init() {
if let data = UserDefaults.standard.data(forKey: "foo"),
let savedFoo = try? JSONDecoder().decode([Bar].self, from: data) {
foo = savedFoo
} else {
foo = []
}
}
func myFunc() {
print("I'm not printing when you delete a row")
}
}
I'm trying to filter an array of Deals with a status DealStaus which has a nested array of Bookings, each one with a BookingStatus.
I want to filter Deals with status .won and Bookings according to the statuses given when calling the function. BookingStatus and DealStatus are both enums.
Deal and Booking look like this:
public struct Deal: Decodable {
public let identifier: String?
public let status: DealStatus
public let bookings: [Booking]?
}
public struct Booking: Decodable {
public let identifier: String?
public let status: BookingStatus
public let startDate: Date?
public let endDate: Date?
}
To do so I wrote the following snippet:
private func getDeals(with bookingStatus: [BookingStatus]) -> [Deal] {
guard let user = currentUser, let deals = user.deals else { return [Deal]() } // Note: user is a class attribute
return deals.filter { $0.status == .won && $0.bookings?.filter { bookingStatus.contains($0.status) }}
}
This does not work. The compiler gives the following error:
Optional type '[Booking]?' cannot be used as a boolean; test for '!=
nil' instead
Following the instructions of #matt, I broke it down:
private func getDeals(with bookingStatus: [BookingStatus]) -> [Deal] {
guard let user = currentUser, let deals = user.deals else { return [Deal]() }
return deals
.filter { $0.status == .won }
.filter { $0.bookings?.contains(where: { bookingStatus.contains($0.status)} ) ?? false }
}